كتابة التطبيقات (الإضافات)

تضيف التطبيقات ميزات إلى Antimatter من الداخل: جزء خادم بلغة Go يتصل بالخادم، وجزء تطبيق ويب بلغة React يضيف قوائم ولوحات وأنواع رسائل وصفحات. تُظهر هذه الصفحة كيفية تنظيم التطبيق، باستخدام تطبيق Polls المدمج كمثال، وما الذي يتغير في واجهة Fusion الخاصة بـ Antimatter.

كيف يعمل التطبيق

  • جزء الخادم هو برنامج بلغة Go. يبدأه الخادم كعملية مستقلة ويتواصل معه عبر RPC: يستدعي الخطافات الخاصة بالتطبيق (رسالة على وشك النشر، تم تنفيذ أمر…)، ويستدعي التطبيق واجهة برمجة التطبيقات الخاصة بالخادم (إنشاء منشور، قراءة مستخدم، تخزين بيانات…). كما يمكنه تقديم مسارات HTTP تحت /plugins/<app id>/.
  • جزء تطبيق الويب هو حزمة JavaScript تُحمَّل في المتصفح مع تطبيق الويب. يسجّل مكوّنات React واستدعاءات مع السجل: زر في قائمة، لوحة، طريقة لرسم نوع رسالة.
  • أي جزء منهما اختياري. يُثبَّت التطبيق كحزمة .tar.gz، ويُمكّن مسؤولو النظام التطبيق ويُعطّلونه ويضبطون إعداداته.
يعمل جزء الخادم مع وصول كامل إلى بيانات الخادم. عامل التطبيق ككود تنشره على الخادم.

تشغّل Antimatter التطبيقات المكتوبة لـ Mattermost، والعكس صحيح، طالما أن التطبيق يستخدم فقط الخطافات التي يمتلكها كلاهما. راجع الخطافات التي تمتلكها Antimatter فقط.

بنية الحزمة

لا يوجد قالب بداية منفصل: ابدأ من تطبيق مدمج صغير مثل antimatter-plugin-polls أو antimatter-plugin-todo، وغيّر معرّفه وكوده. يُنظَّم Polls على النحو التالي:

antimatter-plugin-polls/
├── plugin.json            the manifest
├── Makefile               build, test, bundle, deploy
├── build/                 the manifest and pluginctl helper tools (Go), setup.mk
├── assets/icon.svg        files shipped with the bundle
├── go.mod
├── server/
│   ├── main.go            plugin.ClientMain(&Plugin{})
│   ├── plugin.go          the Plugin type, OnActivate, ServeHTTP
│   ├── manifest.go        generated from plugin.json
│   ├── api.go, command.go, hooks.go, store.go …
│   └── dist/              compiled binaries, one per platform
└── webapp/
    ├── package.json, webpack.config.js, tsconfig.json
    ├── i18n/              en.json and one file per language
    ├── src/index.tsx      registers the app with the web app
    ├── src/manifest.ts    generated from plugin.json
    ├── src/translations.ts
    └── dist/main.js       the bundle

اختياري: مجلد public/، يُقدَّم كما هو عند /plugins/<app id>/public/.

plugin.json

{
  "id": "com.example.weather",
  "name": "Weather",
  "description": "Forecasts in the chat.",
  "homepage_url": "https://github.com/example/weather",
  "support_url": "https://github.com/example/weather/issues",
  "icon_path": "assets/icon.svg",
  "version": "1.0.0",
  "min_server_version": "12.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "linux-arm64": "server/dist/plugin-linux-arm64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "darwin-arm64": "server/dist/plugin-darwin-arm64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    }
  },
  "webapp": {"bundle_path": "webapp/dist/main.js"},
  "settings_schema": {"settings": []}
}
  • id: فريد ودائم؛ يُستخدم عادةً اسم نطاق معكوس. يُسمي المسارات والإعدادات المخزنة وأحداث WebSocket.
  • version: إصدار دلالي. تُترك التطبيقات المدمجة هذا الحقل خارج plugin.json؛ حيث يأخذ أداة البناء قيمته من أحدث وسم git v*.
  • min_server_version: يبلغ Antimatter عن 12.0.0.
  • settings_schema: صفحة إعدادات التطبيق في وحدة التحكم بالنظام (انظر الإعدادات).

ينسخ make apply الملف التعريفي إلى server/manifest.go وwebapp/src/manifest.ts؛ ويتحقق make manifest-check من صحته.

جزء الخادم

يستورد جزء الخادم نفس وحدة Go التي تستخدمها تطبيقات Mattermost، وهي github.com/mattermost/mattermost/server/public، من وكيل وحدات Go. ووظيفة main هي بدء تشغيل التطبيق فقط:

package main

import "github.com/mattermost/mattermost/server/public/plugin"

func main() {
	plugin.ClientMain(&Plugin{})
}

يندمج نوع Plugin مع plugin.MattermostPlugin، مما يمنحه p.API، ويحقق في الخطافات التي يحتاجها:

type Plugin struct {
	plugin.MattermostPlugin
	router *mux.Router
}

func (p *Plugin) OnActivate() error {
	p.router = p.newRouter()
	return p.API.RegisterCommand(&model.Command{
		Trigger:          "weather",
		AutoComplete:     true,
		AutoCompleteDesc: "Show the forecast",
		AutoCompleteHint: "[city]",
	})
}

func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
	city := strings.TrimSpace(strings.TrimPrefix(args.Command, "/weather"))
	return &model.CommandResponse{
		ResponseType: model.CommandResponseTypeEphemeral,
		Text:         "Forecast for " + city + ": sunny",
	}, nil
}

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
	p.router.ServeHTTP(w, r)
}

الخطافات الشائعة

الخطافمتى يعمل
OnActivate, OnDeactivateعند تفعيل التطبيق أو تعطيله، وعند بدء تشغيل الخادم وإيقافه. سجّل الأوامر والبوتات في OnActivate.
OnConfigurationChangeعند تغيير إعداداته؛ حمّلها باستخدام p.API.LoadPluginConfiguration(&cfg).
ServeHTTPالطلبات الموجهة إلى /plugins/<app id>/…. يضبط الخادم ترويسة Mattermost-User-Id عندما يأتي الطلب من مستخدم مسجّل الدخول؛ ارفض الطلبات التي لا تحتوي عليها ما لم تكن مخصصة للعامة.
ExecuteCommandعند تنفيذ أمر سجّله التطبيق.
MessageWillBePosted, MessageWillBeUpdatedقبل حفظ رسالة: أعد رسالة معدّلة، أو سبباً لرفضها.
MessageHasBeenPosted, MessageHasBeenUpdated, MessageHasBeenDeletedبعد التنفيذ، للأثار الجانبية.
ReactionHasBeenAdded, ReactionHasBeenRemovedالردود الفعل.
UserHasJoinedChannel, UserHasLeftChannel, UserHasJoinedTeam, UserHasLeftTeam, ChannelHasBeenCreatedالتغييرات في العضوية والقنوات. الفريق هو مجتمع.
FileWillBeUploadedقبل تخزين ملف: عدّله أو ارفضه.
OnPluginClusterEventرسالة من نفس التطبيق على خادم آخر في العنقود.

القائمة الكاملة، مع التواقيع الدقيقة، موجودة في server/public/plugin/hooks.go؛ والواجهة البرمجية موجودة في server/public/plugin/api.go. أما حزمة pluginapi (server/public/pluginapi) فتغلّف الواجهة البرمجية بمساعدات أسهل في الاستخدام.

نداءات الواجهة البرمجية المفيدة

  • CreatePost، UpdatePost، SendEphemeralPost: النشر باسم بوت التطبيق أو باسم مستخدم.
  • EnsureBotUser(&model.Bot{Username, DisplayName}): أنشئ بوت التطبيق مرة واحدة واحصل على معرّفه.
  • PublishWebSocketEvent("poll_updated", payload, &model.WebsocketBroadcast{ChannelId: id}): أرسل حدثاً إلى تطبيق الويب. يصل الحدث باسم custom_<app id>_poll_updated.
  • OpenInteractiveDialog: افتح مربع حوار.
  • LogInfo، LogError وما شابه: اكتب في سجل الخادم، مع وسمه بمعرّف التطبيق.
  • GetUser، GetChannel، HasPermissionToChannel: تحقق ممن يستطيع القيام بما قبل التصرف نيابة عن مستخدم.

يعمل التطبيق على كل خادم في العنقود. احفظ الحالة في مخزن المفاتيح والقيم أو في قاعدة البيانات، وليس في الذاكرة فقط.

خدمة الذكاء الاصطناعي

يستدعي خادم Antimatter نماذج الذكاء الاصطناعي للتطبيقات، من خلال اتصالات النماذج التي يعدها المدير (انظر الذكاء الاصطناعي في دليل الإدارة). لا يحتفظ التطبيق أبدًا بمفتاح مزود الخدمة: بل يحدد اسم الشخص الذي يتصرف نيابة عنه وأين، ويتحقق الخادم من أن هذا الشخص مسموح له باستخدام الاتصال هناك، ويطبق حدوده وميزانياته، ويحسب الرموز مقابل الشخص والتطبيق، ويدقق المكالمة دون محتواها.

الطريقةوظيفته
AIChat(actingUserID, channelID, teamID, *ai.ChatRequest)إكمال محادثة: رسائل، أدوات، مخرجات منظمة (JSONSchema).
AIChatStream(…)نفس الشيء، ولكن بتدفق مستمر: اقرأ Events التدفق حتى تغلق القناة، أو Close لإيقاف المكالمة.
AIEmbed(…, *ai.EmbedRequest)تضمين النصوص (Embeddings).
AITranscribe(…, *ai.TranscribeRequest)تحويل الكلام إلى نص.
AIDecide(…, *ai.DecideRequest)أسئلة مكتوبة إلى نموذج قرار (نعم أو لا، خيار، درجة) حول نص.
AIConnectionsForUser(userID, channelID, teamID)الاتصالات التي يمكن للشخص استخدامها هناك، مع نماذجها وقدراتها، وليس مفاتيحها أبدًا.

يحدد ConnectionID لكل طلب الاتصال، ويحدد Model أحد نماذجه (فارغًا للاتصال الافتراضي). Operation هي تسمية قصيرة لسجلات الاستخدام والتدقيق، وليست محتوى أبدًا. أدنى إصدار خادم مطلوب هو 11.11.

خذ actingUserID فقط من مصدر تحقق منه الخادم: ترويسة Mattermost-User-Id في طلب إلى ServeHTTP الخاص بك، أو مستخدم سياق الخطاف (مؤلف المنشور، المستخدم الذي نفذ أمرًا). لا تأخذه أبدًا من رسالة، أو حمولة خطاف ويب، أو عنوان URL، أو طلب من تطبيق آخر: وإلا فإن تطبيقك سيستهلك وصول شخص آخر.

توجد هذه الطرق وحزمة ai (server/public/ai) فقط في نسخة Antimatter من الوحدة، وليس في النسخة المنشورة بواسطة Mattermost. لاستخدامها، قم بالبناء مقابل server/public الخاص بـ Antimatter باستخدام سطر replace في ملف go.mod الخاص بك، على سبيل المثال replace github.com/mattermost/mattermost/server/public => ../antimatter/server/public. مثل هذا التطبيق يعمل فقط على Antimatter.

تزويد الوكلاء بالأدوات

تقدم التطبيقات أدواتها الخاصة للوكلاء باستخدام RegisterAITools([]*model.AIToolDefinition): حيث تتضمن كل أداة اسمها، وعنوانها، ووصفها، ومخطط إدخال JSON، ونوعها (read أو write أو external_write)، وهو ما يحدد ما إذا كان بإمكان الوكيل تشغيلها بشكل مستقل وفقًا لسياساته (راجع قسم الأدوات في دليل المشرفين). يتم التحقق من الأذونات بناءً على وسيط القناة أو المجتمع في الاستدعاء، أو على مستوى الخادم، حسب حاجة الأداة. عندما يستخدم الوكيل الأداة، يستدعي الخادم الخطاف ExecuteAITool(c, *model.AIToolExecution) *model.AIToolResult مع تحديد هوية الشخص الذي قدم الطلب في الحقل UserId: يجب أن تعمل الأداة بصلاحيات هذا الشخص فقط، وليس بصلاحيات الوكيل. تُسمّى الأدوات بالصيغة <app id>.<name>؛ وتتطلب إصدار خادم 11.11 على الأقل؛ هذه الطرق والخطاف موجودان فقط في server/public الخاص بـ Antimatter، مثل خدمة الذكاء الاصطناعي المذكورة أعلاه. لا يمكن للتطبيقات التي يكون معرّفها antimatter أو التي تبدأ بـ mcp. تسجيل الأدوات.

تخزين البيانات

يحتوي كل تطبيق على مخزن مفاتيح وقيم خاص به في قاعدة بيانات الخادم:

  • KVSet(key, value)، KVGet(key)، KVDelete(key): تكون القيم بايتات، وعادةً ما تكون بصيغة JSON. لا تتجاوز المفاتيح 150 حرفًا.
  • KVSetWithExpiry(key, value, seconds): تختفي القيمة بعد انقضاء هذه المدة.
  • KVCompareAndSet(key, old, new) وKVSetWithOptions مع Atomic: تحديثات آمنة عندما تكتب عدة طلبات أو خوادم إلى المفتاح نفسه. يحفظ استطلاع كل صوت بهذه الطريقة ويعيد المحاولة عند التعارض.
  • KVList(page, perPage): المفاتيح، صفحةً بصفحة.

جزء تطبيق الويب

يسجّل الحزمة (bundle) فئةً باستخدام window.registerPlugin. تستقبل initialize السجل (registry) ومخزن Redux:

import manifest from './manifest';

class Plugin {
    initialize(registry, store) {
        registry.registerTranslations((locale) => translationsFor(locale));
        registry.registerPostTypeComponent('custom_poll', PollPost);
        registry.registerWebSocketEventHandler(`custom_${manifest.id}_poll_updated`, (msg) => {
            store.dispatch(pollUpdated(JSON.parse(msg.data.poll)));
        });
        registry.registerReconnectHandler(() => store.dispatch(reloadPolls()));
    }
}

window.registerPlugin(manifest.id, new Plugin());
  • المكتبات المشتركة. يوفر تطبيق الويب React وReactDOM وRedux وReact Redux وReact Intl: اذكرها كـ externals في webpack (React، ReactDOM، Redux، ReactRedux، ReactIntl) بدلاً من تضمين نسخ خاصة بك. راجع webpack.config.js الخاص باستطلاع.
  • الاتصال بجزء الخادم الخاص بك. استخدم Fetch لجلب ${window.basename}/plugins/<app id>/… مع الخيارات من Client4.getOptions() (من mattermost-redux/client): فهي تحمل الترويسات التي يحتاجها الخادم لقبول طلب من المتصفح.
  • الحالة الخاصة بك. يضيف registerReducer مُخفِّضًا (reducer) يُخزَّن تحت state['plugins-<app id>'].
  • الأنواع. طرق السجل الإضافية في Antimatter غير موجودة في الأنواع المنشورة في Mattermost: يحدد استطلاع السجل الذي يستخدمه في webapp/src/types/host.ts.

يتم توثيق طرق السجل وبارامتراتها في الكود، في webapp/fusion/src/plugins/registry.ts (Fusion) وwebapp/channels/src/plugins/registry.ts (الكلاسيكي).

دعم واجهة Fusion

يحتوي Antimatter على واجهتي ويب: Fusion، وهي الافتراضية، والكلاسيكية. تحمّل كلتا الواجهتين حزم التطبيقات نفسها ولديهما السجل نفسه، لكن Fusion ترسم العديد من العناصر بنفسها وتضع مكوّنات الإضافات بطريقتها الخاصة. اختبر تطبيقك في كلتا الواجهتين.

كشف الواجهة

قبل تحميل أي تطبيق، تضبط كلتا الواجهتين window.antimatterWebUI على 'fusion' أو 'classic'، وتضبط القيمة نفسها على عنصر الصفحة الجذر كـ data-am-web-ui. في Mattermost، لا يُضبط أي منهما.

export function isFusionUI(): boolean {
    return window.antimatterWebUI === 'fusion' || document.documentElement.dataset.amWebUi === 'fusion';
}

تستخدم التطبيقات المدمجة هذا لعرض واجهات مختلفة. تعرض تطبيق الاستطلاعات بطاقات الاستطلاع بأسلوب Fusion، وفي Fusion يتم كتابة الاستطلاع في محرر الرسائل (وضع من أوضاع المحرر) بدلاً من نافذة حوارية. تتجاهل تطبيقات المكالمات وقنوات الصوت مكونات الشريط الجانبي والإشعارات المنبثقة التي تستبدلها Fusion بواجهة المكالمات الخاصة بها. يضع تطبيق محادثة الذكاء الاصطناعي إجراءات الذكاء الاصطناعي في قائمة الرسائل، كمجموعة تحت إجراءات أخرى، بدلاً من شريط الأدوات عند التمرير. تستخدم هذه التطبيقات فئات CSS الخاصة بـ Fusion (التي تبدأ بـ am-) وطبقة النوافذ الحوارية الخاصة بها (العنصر am-layer) لمطابقة الواجهة؛ وهي ليست واجهة برمجة تطبيقات موثقة ومستقرة.

أين تعرض Fusion مكوّنات الإضافات

طريقة التسجيلأين يظهر في Fusion
registerAppBarComponent, registerChannelHeaderButtonActionسكة التطبيقات، وهي عمود الأيقونات على الحافة اليمنى، بين صورتك الرمزية وزر حول Antimatter. تذهب أزرار ترويسة القناة هناك أيضاً، وليس في ترويسة القناة. يُخفى عندما يفعّل مدير النظام تعطيل شريط التطبيقات (ExperimentalSettings.DisableAppBar، معطّل افتراضياً).
registerRightHandSidebarComponentاللوحة الجانبية اليمنى. رأسها خاص بـ Fusion، وهو نفسه لجميع التطبيقات: أيقونة التطبيق واسمه (العنوان المسجل عندما يعرض نصًا فقط، وإلا يكون تسمية شريط التطبيقات أو الاسم في البيان التعريفي)، ثم استعادة حجم اللوحة (بعد تغيير الحجم)، فتح في نافذة جديدة وإغلاق. يتم تجاهل أي عنوان يعرض عناصر أخرى (عنصر تحكم، رابط، صورة)، لذا يجب عليك عرض عناصر التحكم الخاصة بتطبيقك داخل اللوحة نفسها، على سبيل المثال في شريط رفيع في أعلى اللوحة. يمكن للمستخدمين تغيير حجم اللوحة عن طريق سحب حافتها. راجع اللوحة في نافذة جديدة.
registerChannelHeaderIconأيقونات في ترويسة القناة، بعد أزرار المكالمات؛ وعلى الهواتف، في قائمة المزيد في الترويسة.
registerCallButtonActionزر المكالمة في ترويسة القناة؛ تتشارك عدة مزوّدين قائمة طرق أخرى للمكالمة.
registerPostDropdownMenuActionقائمة إجراءات الرسالة (⋯) الخاصة بالرسالة، ضمن القائمة الفرعية إجراءات أخرى؛ كما تظهر أيضًا في قائمة إجراءات سلسلة الرسائل ضمن لوحة سلسلة الرسائل، تحت إجراءات أخرى. تعرض Fusion أيقونة التطبيق قبل كل إجراء، باللون الرمادي: أيقونتها الخاصة للتطبيقات المدمجة، وإلا أيقونة التطبيق كما تظهر في شريط التطبيقات، وإلا أيقونة محايدة. يتم إخفاء أي عنصر ليس نصًا في بداية نص الإجراء (أي أيقونة يرسمها التطبيق بنفسه).
registerPostDropdownSubMenuActionمجموعة من الإدخالات في القائمة الفرعية إجراءات أخرى نفسها، بعد الإجراءات الفردية، مفصولة بخط، وبدون عنوان. لا يحصل أي تطبيق على إدخال خاص به في قائمة الرسالة.
registerPostActionComponentشريط الأدوات الذي يظهر عند التحويم فوق الرسالة، قبل زر ⋯.
registerPostTypeComponent, registerPostMessageAttachmentComponent, registerPostHeaderComponent, registerPostWillRenderEmbedComponentالرسالة نفسها: جسمها، أسفل نصها، بعد وقتها، وفي مكان معاينات الروابط.
registerChannelHeaderMenuActionقائمة خيارات القناة (⋯ على قناة في الشريط الجانبي، أو النقر بزر الفأرة الأيمن)، كعناصر بأيقونة قابس.
registerMainMenuActionقائمة المجتمع (اسم المجتمع في أعلى قائمة القنوات)، تحت إجراءات أخرى، مجمّعة حسب التطبيق.
registerSidebarBrowseOrAddChannelMenuActionقائمة المجتمع، بعد إنشاء قناة وإنشاء تصنيف.
registerFileUploadMethodقائمة + في محرر الرسائل (إضافة إلى الرسالة)، بعد رفع ملف.
registerPostEditorActionComponentشريط التنسيق في محرر الرسائل، عند إظهار التنسيق.
registerAIActionMenuItemComponentقائمة الذكاء الاصطناعي في محرر الرسائل.
registerFileDropdownMenuAction, registerFilePreviewComponentقائمة خيارات الملف، ومعاينة الملف.
registerUserSettingsالملف الشخصي والإعدادات، تبويب واحد لكل تطبيق تحت عنوان الإضافات.
registerChannelSettingsTabنافذة إعدادات القناة، تحت عنوان التطبيقات.
registerPopoverUserAttributesComponent, registerPopoverUserActionsComponentبطاقة الملف الشخصي التي تظهر عند النقر على اسم أو صورة رمزية.
registerLeftSidebarHeaderComponent, registerSidebarChannelLinkLabelComponent, registerChannelIconOverride, registerBottomTeamSidebarComponentأعلى قائمة قنوات المجتمع، بجانب اسم القناة، وأيقونة القناة، وأسفل شريط المجتمع على اليسار.
registerChannelIntro, registerChannelIntroButtonAction, registerChannelToastComponent, registerNewMessagesSeparatorActionComponent, registerChannelComposerBannerComponent, registerComposerPlaceholderبداية سجل القناة، أعلى قائمة الرسائل، خط الرسائل غير المقروءة، فوق محرر الرسائل، ونص المحرر الاحتياطي.
registerChannelTypeOptionأنواع القنوات في مربع حوار إنشاء قناة.
registerSearchComponentsمربع البحث في ترويسة القناة: تظهر نطاقات التطبيق عندما يكون فارغاً.
registerUserGuideDropdownMenuActionحول Antimatter (أسفل شريط التطبيق)، تحت المساعدة.
registerProductصفحات كاملة ضمن عنوان URL الخاص بالمنتج، في إطار الصفحة الكلاسيكي، بدون قائمة منتجات: لا توفر Fusion أي قائمة، ولا تعرض قائمة بالمنتجات في أي مكان، لذا قم بربط الصفحة بنفسك. لا يسجل أي تطبيق مدمج كمنتج: لوحات كانبان، وأدلة التشغيل، ودردشة الذكاء الاصطناعي تفتح في لوحاتها الجانبية.
registerRootComponent، registerGlobalComponent، خطافات مثل registerMessageWillBePostedHook، registerSlashCommandWillBePostedHook، registerMessageWillFormatHook، registerFilesWillUploadHook، registerDesktopNotificationHook، registerWebSocketEventHandler، registerReducer، registerTranslationsتعمل كما في الواجهة الكلاسيكية.
registerAdminConsolePlugin, registerAdminConsoleCustomSettingوحدة التحكم بالنظام، وهي الكلاسيكية في كلا الواجهتين.

ما لا يعرضه Fusion أو يشغّله

  • لا يتم عرض registerPostDropdownMenuComponent (مكوّن في قائمة الرسالة) وregisterPostCardTypeComponent (بطاقة المنشور في اللوحة اليمنى) في العروض الرئيسية لـ Fusion. لا يفعل registerProductSwitcherMenuItem شيئًا في Fusion، التي لا تحتوي على قائمة منتجات (تسجل تحذيرًا). استخدم registerPostDropdownMenuAction بدلاً من الأول.
  • لا يعمل registerMessageWillBeUpdatedHook عند تحرير رسالة في Fusion. افحص التعديلات على الخادم باستخدام خطاف MessageWillBeUpdated بدلاً من ذلك.
  • يعمل registerActionAfterChannelCreation فقط من مربع حوار إنشاء القناة الكلاسيكي، والذي يفتحه Fusion من خيارات أخرى… في مربع حوار إنشاء قناة الخاص به.
  • تُعرض مكوّنات شريط التطبيق بغضّ النظر عن قيمة supportedProductIds الخاصة بها.
  • تحافظ المسارات من registerNeedsTeamRoute وregisterCustomRoute على إطار الصفحة الكلاسيكي، تماماً مثل المنتجات.

خطافات حصرية لـ Antimatter

لا توجد هذه طرق التسجيل في Mattermost. تأكد من وجود طريقة قبل استدعائها، حتى يعمل التطبيق هناك أيضاً:

if (typeof registry.registerComposerMenuItem === 'function') {
    registry.registerComposerMenuItem({
        text: 'Create a poll',
        icon: <i className='icon icon-chart-bar'/>,
        fusionIcon: 'poll',
        action: ({channelId, rootId}) => openPollCreation(channelId, rootId),
    });
} else {
    registry.registerChannelHeaderButtonAction(icon, (channel) => openPollCreation(channel.id), 'Create a poll', 'Create a poll');
}
الطريقةما تضيفه
registerComposerMenuItem({text, icon, fusionIcon, action, shouldRender})عنصر في قائمة + في صندوق الرسائل، في كلا الواجهتين. يستقبل action قيمة {channelId, rootId}.
registerEmojiPickerTab({id, label, icon, fusionIcon, order, component, shouldRender, replacesGifPicker})تبويب من منتقي الرموز التعبيرية في مربع الرسالة، مثل ملفات GIF أو الملصقات. في Fusion، تأتي التبويبات ذات order أقل من 10 قبل تبويب الرموز التعبيرية. يتلقى المكوّن channelId، وrootId، وfilter، وonFilterChange، وonSelectDone، وinsertText، وisFusion، وفي Fusion، pickerKit (انظر أدناه).
registerChannelViewPanel({matcher, component})لوحة بين ترويسة القناة والرسائل، للقنوات التي يختارها matcher(state, channel). يمكنها إخفاء الرسائل والمحرر (setMessagesVisible)، كما تفعل القنوات الصوتية.
registerSidebarChannelFooterComponent(component)سطر تحت قناة في الشريط الجانبي، مثل الأشخاص في مكالمتها. يستقبل channel.
registerIntegrationsOption({title, description, fusionIcon, action, shouldRender})خاص بفوزيون فقط: عنصر في صفحات التكاملات، بجوار الرموز التعبيرية المخصصة. تضيف تطبيق GIF رموزه التعبيرية وملصقاته المخصصة هناك.

إضافة وإدارة العروض في منتقي الرموز التعبيرية

في Fusion، تحصل علامة تبويب منتقي الرموز التعبيرية على الخاصية pickerKit: وهي المكوّنات التي يستخدمها تبويب الرموز التعبيرية لعرضي الإضافة والإدارة الخاصين به، بحيث تبدو عروض التبويب متطابقة وتبقى داخل المنتقي. تستخدم تطبيقي GIFs وMemes هذه الخاصية لإدارة وإضافة ملفات GIF والملصقات وقوالب الصور الساخرة (Memes). تحتوي على version (1)، والخطافات usePickerView وuseOnEscape، والمكوّنات PickerViewButtons (الزر + وزر الإدارة بجانب مربع البحث)، وPickerViewFrame (عرض مع سهم الرجوع)، وPickerNotice، وPickerField، وPickerSearch، وPickerDropZone، وPickerManageList. يؤدي الضغط على Escape إلى الرجوع من عرض إلى شبكة التبويب. تأكد من وجود هذه الخاصية: فالمضيفون الآخرون يتجاهلونها، ويحتفظ التبويب حينها بنوافذه الحوارية الخاصة. توجد الأنواع في webapp/fusion/src/types/store/plugins.ts (EmojiPickerTabProps)، والمكوّنات في webapp/fusion/src/fusion/popovers/picker_views.tsx.

أوضاع المحرر

في Fusion، تضيف الدالة registerComposerMode({id, label, fusionIcon, placeholder, sendLabel, component, shouldRender}) طريقة أخرى لكتابة رسالة في المحرر، كما يفعل تطبيق الاستطلاعات (Polls) لإنشاء استطلاع. يُعرض الوضع في قائمة + الخاصة بالمحرر تحت label الخاص به. بمجرد اختياره، يحتفظ المحرر بمربع النص الخاص به (مع placeholder الخاص بالوضع: سؤال الاستطلاع)، ويعرض component الخاص بك أسفله، وشريطًا يسمي الوضع مع زر إلغاء؛ ويتلقى زر إرسال قيمة sendLabel ويشغّل دالة الإرسال الخاصة بك بدلاً من نشر النص.

  • يتلقى المكوّن channelId، وrootId (سلسلة الرسائل، في مربع الرد)، وmessage (نص المحرر)، وsetSubmit، وonCancel (للعودة إلى رسالة عادية مع الاحتفاظ بالنص).
  • مرر إلى setSubmit دالة تُستدعى مع نص المحرر وتُحل إلى لا شيء (أو سلسلة فارغة) بمجرد الإرسال: عندئذٍ يفرغ المحرر ويعود إلى رسالة عادية. حل الدالة إلى سلسلة لعرضها كخطأ في المكان نفسه.
  • كل من placeholder وsendLabel هما سلسلتان، أو دالتان تعتمدان على intl الخاص بمكتبة react-intl. تحدد الدالة shouldRender(state, {channelId, rootId}) ما إذا كان المحرر يعرض هذا الوضع أم لا.
  • لفتح الوضع من مكان آخر، مثل أمر شرطة مائلة بدون وسائط: window.dispatchEvent(new CustomEvent('am-composer-mode', {detail: {id, channelId, rootId}})).

تدعم Fusion فقط أوضاع المحرر: تحقق من وجود الدالة في السجل، وإلا احتفظ بنافذتك الحوارية الخاصة بك. تُرجع الطريقة معرّف الوضع، ويُزال الوضع عند إزالة التطبيق.

اللوحة في نافذة جديدة

يمنح Fusion المكوّن registerRightHandSidebarComponent خاصيتين إضافيتين، بحيث يفتح فتح في نافذة جديدة اللوحة في مكانها الحالي بدلاً من الصفحة الرئيسية للتطبيق:

  • setPanelState(state): استدعِها عند تغيير موضع اللوحة، مع نص قصير يعرّفه تطبيقك، مثل 'note/<id>' أو 'board/<id>/card/<id>'، أو null عند الصفحة الرئيسية. يُدرج هذا النص في عنوان النافذة، لذا لا تضع فيه أبداً بيانات سرية. تُهمل الحالات التي يتجاوز طولها 512 حرفاً أو تحتوي على أحرف تحكم.
  • initialPanelState: في النافذة الجديدة، يمثل حالة اللوحة السابقة، أو undefined. تعامل معه كمدخل غير موثوق به: أعد جلب العنصر بصفتك المستخدم المسجّل دخوله، واعرض الصفحة الرئيسية للتطبيق أو عرض «لم يتم العثور عليه» إذا لم يتمكن من فتحه.

تُنسى الحالة عند إغلاق اللوحة أو عند استبدالها بلوحة تطبيق آخر. لفتح نافذة لوحة بنفسك، تقبل popoutRhsPlugin (الموجودة أيضاً في window.WebappUtils.popouts) الحالة كوسيط اختياري خامس. تستخدم هذا الميزات التالية: الملاحظات، والسبورة البيضاء، وقائمة المهام، ولوحات كانبان، وأدلة التشغيل.

يُسمّي fusionIcon أيقونة من مجموعة أيقونات Fusion (مثل poll، sticker، calendar أو image؛ راجع webapp/fusion/src/fusion/components/icon). يعرضها Fusion بدلاً من icon، حتى يتطابق العنصر مع غيره.

الإعدادات

يصف settings_schema في plugin.json صفحة التطبيق في وحدة التحكم بالنظام › الإضافات:

"settings_schema": {
  "header": "Forecasts come from the weather service below.",
  "settings": [
    {"key": "ServiceURL", "display_name": "Service URL", "type": "text", "default": "https://api.example.com"},
    {"key": "Units", "display_name": "Units", "type": "dropdown", "default": "metric",
     "options": [{"display_name": "Celsius", "value": "metric"}, {"display_name": "Fahrenheit", "value": "imperial"}]},
    {"key": "EnableAlerts", "display_name": "Severe weather alerts", "type": "bool", "default": false,
     "help_text": "Post alerts in the channels that follow a city."}
  ]
}

أنواع الإعدادات: bool، text، longtext، number، dropdown، radio، generated (سر عشوائي)، username، وcustom (يرسمها مكوّن تسجّلهه جزء تطبيق الويب باستخدام registerAdminConsoleCustomSetting). يمكن تجميع الإعدادات في sections.

تقرأ جزء الخادم هذه الإعدادات في بنية بيانات (struct) تطابق حقولها المفاتيح:

type configuration struct {
	ServiceURL   string
	Units        string
	EnableAlerts bool
}

func (p *Plugin) OnConfigurationChange() error {
	var cfg configuration
	if err := p.API.LoadPluginConfiguration(&cfg); err != nil {
		return err
	}
	p.setConfiguration(&cfg)
	return nil
}

يمكن للمديرين أيضاً ضبطها في الإعدادات، تحت PluginSettings.Plugins.<app id>، بمفاتيح بحروف صغيرة (راجع دليل المديرين).

الترجمات

يُترجم Antimatter إلى 37 لغة، وكذلك التطبيقات المضمّنة. يقوم جزء تطبيق الويب بذلك على النحو التالي:

  1. اكتب كل نص باستخدام React Intl، مع معرّف ونصه الإنجليزي: <FormattedMessage id='polls.create' defaultMessage='Create a poll'/> أو defineMessages. ابدأ المعرّفات باسم تطبيقك.
  2. استخرج النصوص الإنجليزية إلى webapp/i18n/en.json (تستخدم التطبيقات المضمّنة formatjs extract).
  3. أضف ملفاً واحداً لكل لغة بجانبه، بنفس المعرّفات: fr.json، de.json، pt-BR.json، zh-CN.json…
  4. أعطِها لتطبيق الويب، الذي يطلب لغة المستخدم:
    import fr from '../i18n/fr.json';
    import ptBR from '../i18n/pt-BR.json';
    
    const catalogs = {fr, 'pt-BR': ptBR};
    
    export function translationsFor(locale) {
        return catalogs[locale] || catalogs[locale.split('-')[0]] || {};
    }
    
    registry.registerTranslations(translationsFor);

إذا كان النص مفقودًا، يُعاد إلى القيمة الافتراضية الإنجليزية defaultMessage. بالنسبة إلى النصوص التي يرسلها الجزء الخادمي (إجابات الأوامر، رسائل البوت)، يحمّل حزمة pluginapi/i18n ملفات الترجمة المرفقة بالحزمة (مثل assets/i18n) ويوفّر مُحوِّلًا لكل مستخدم بلغته.

البناء، التثبيت، وتصحيح الأخطاء

تحتاج إلى Go وNode.js (الإصدار الموجود في .nvmrc الخاص بالتطبيق). في مجلد التطبيق:

الأمروظيفته
make distيبني الجزء الخادمي لكل المنصّات، وجزء تطبيق الويب، ويكتب الحزمة في dist/<app id>-<version>.tar.gz.
make testيشغّل اختبارات Go وJavaScript.
make check-styleيفحص الكود (golangci-lint, ESLint) ويتحقق من أنواع TypeScript.
make deployيبني، ثم يثبّت الحزمة على الخادم ويفعّلها.
make watch, make deploy-from-watchالأول يعيد بناء جزء تطبيق الويب كل مرة تحفظ فيها؛ والثاني يحزم ما بُني ويثبّته على الخادم.
make enable, make disable, make resetفعّل، عطّل، أو أعد تشغيل التطبيق على الخادم.
make logs, make logs-watchيعرض أسطر التطبيق في سجل الخادم.
make attachيربط مُصحِّح Delve بالجزء الخادمي قيد التشغيل (ابنِ أولاً مع AM_DEBUG=1).

يستخدم make deploy وأوامر الخادم الأخرى مقبس وضع محلي إذا وجدوا واحدًا (/var/tmp/antimatter_local.socket، أو AM_LOCALSOCKETPATH): شغّلها على جهاز الخادم مع تفعيل الوضع المحلي. وإلا، تسجّل الدخول إلى الخادم المحدد بـ MM_SERVICESETTINGS_SITEURL باستخدام AM_ADMIN_TOKEN، أو AM_ADMIN_USERNAME وAM_ADMIN_PASSWORD. أدوات البناء لا تقرأ بعد اسم AM_ لعنوان الموقع.

تثبيت حزمة يدويًا

  • يُتيح مدير النظام الرفع باستخدام AM_PLUGINSETTINGS_ENABLEUPLOADS=true (معطّل افتراضيًا؛ لا يوجد مفتاح في وحدة التحكم بالنظام).
  • ثم وحدة التحكم بالنظام › الإضافات › إدارة الإضافات › رفع إضافة، أو amctl plugin add weather.tar.gz متبوعًا بـ amctl plugin enable com.example.weather.
  • إذا كانت الاشتراط توقيع الإضافة مفعّلة على الخادم، فإنه يقبل فقط الحزم الموقّعة بمفتاح يثق به؛ راجع تثبيت تطبيقات أخرى.
للإصدارات السريعة على خادم تطوير، اضبط AM_SERVICESETTINGS_ENABLEDEVELOPER=true على الخادم: ستحمل الأخطاء حينها تفاصيلها. ملفات Makefile للتطبيقات المرفقة تبني الجزء الخادمي لمنصّتك فقط إذا كان MM_SERVICESETTINGS_ENABLEDEVELOPER مضبوطًا في شلّك.