@zlr_236/popo 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/channel.ts ADDED
@@ -0,0 +1,839 @@
1
+ import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
3
+ import type { ResolvedPopoAccount, PopoConfig } from "./types.js";
4
+ import { resolvePopoAccount, resolvePopoCredentials } from "./accounts.js";
5
+ import { popoOutbound } from "./outbound.js";
6
+ import { probePopo } from "./probe.js";
7
+ import { resolvePopoGroupToolPolicy } from "./policy.js";
8
+ import { normalizePopoTarget, looksLikePopoId } from "./targets.js";
9
+ import { sendMessagePopo, updateInstructionVariableOptions } from "./send.js";
10
+ import { recallMessagePopo, getMessageReadAckPopo, downloadMessageFilePopo, configureCardCallbackPopo } from "./media.js";
11
+ import {
12
+ createTeam,
13
+ inviteToTeam,
14
+ dropTeam,
15
+ getTeamMembers,
16
+ getTeamInfo,
17
+ updateTeamInfo,
18
+ updateTeamManagement,
19
+ getViewScope,
20
+ modifyViewScope,
21
+ publishRobot,
22
+ } from "./team.js";
23
+
24
+ const meta = {
25
+ id: "popo",
26
+ label: "POPO",
27
+ selectionLabel: "POPO (网易)",
28
+ docsPath: "/channels/popo",
29
+ docsLabel: "popo",
30
+ blurb: "POPO enterprise messaging.",
31
+ aliases: [],
32
+ order: 80,
33
+ } as const;
34
+
35
+ export const popoPlugin: ChannelPlugin<ResolvedPopoAccount> = {
36
+ id: "popo",
37
+ meta: {
38
+ ...meta,
39
+ },
40
+ pairing: {
41
+ idLabel: "popoEmail",
42
+ normalizeAllowEntry: (entry) => entry.replace(/^(popo|user):/i, ""),
43
+ notifyApproval: async ({ cfg, id }) => {
44
+ await sendMessagePopo({
45
+ cfg,
46
+ to: id,
47
+ text: PAIRING_APPROVED_MESSAGE,
48
+ });
49
+ },
50
+ },
51
+ capabilities: {
52
+ chatTypes: ["direct", "channel"],
53
+ polls: false,
54
+ threads: false,
55
+ media: true,
56
+ reactions: false,
57
+ edit: false,
58
+ reply: true,
59
+ },
60
+ agentPrompt: {
61
+ messageToolHints: () => [
62
+ "- POPO targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:email@example.com` or `group:groupId`.",
63
+ "- POPO actions: send, card, unsend (target=to), addParticipant (target=tid), channel-info/channel-edit/channel-delete (channelId=tid), channel-create (no target), member-info (tid param). Custom actions without target: read-ack (msgId), download-file (fileId), update-team-mgmt, view-scope, modify-view-scope, publish-robot, update-instruction-options, configure-card-callback.",
64
+ ],
65
+ },
66
+ groups: {
67
+ resolveToolPolicy: resolvePopoGroupToolPolicy,
68
+ },
69
+ reload: { configPrefixes: ["channels.popo"] },
70
+ configSchema: {
71
+ schema: {
72
+ type: "object",
73
+ additionalProperties: false,
74
+ properties: {
75
+ enabled: { type: "boolean" },
76
+ systemPrompt: { type: "string" },
77
+ appKey: { type: "string" },
78
+ appSecret: { type: "string" },
79
+ token: { type: "string" },
80
+ aesKey: { type: "string" },
81
+ server: { type: "string" },
82
+ webhookPath: { type: "string" },
83
+ webhookPort: { type: "integer", minimum: 1 },
84
+ dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
85
+ allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
86
+ groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
87
+ groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
88
+ requireMention: { type: "boolean" },
89
+ historyLimit: { type: "integer", minimum: 0 },
90
+ dmHistoryLimit: { type: "integer", minimum: 0 },
91
+ textChunkLimit: { type: "integer", minimum: 1 },
92
+ chunkMode: { type: "string", enum: ["length", "newline"] },
93
+ mediaMaxMb: { type: "number", minimum: 0 },
94
+ renderMode: { type: "string", enum: ["raw", "rich_text"] },
95
+ },
96
+ },
97
+ },
98
+ config: {
99
+ listAccountIds: () => [DEFAULT_ACCOUNT_ID],
100
+ resolveAccount: (cfg) => resolvePopoAccount({ cfg }),
101
+ defaultAccountId: () => DEFAULT_ACCOUNT_ID,
102
+ setAccountEnabled: ({ cfg, enabled }) => ({
103
+ ...cfg,
104
+ channels: {
105
+ ...cfg.channels,
106
+ popo: {
107
+ ...cfg.channels?.popo,
108
+ enabled,
109
+ },
110
+ },
111
+ }),
112
+ deleteAccount: ({ cfg }) => {
113
+ const next = { ...cfg } as ClawdbotConfig;
114
+ const nextChannels = { ...cfg.channels };
115
+ delete (nextChannels as Record<string, unknown>).popo;
116
+ if (Object.keys(nextChannels).length > 0) {
117
+ next.channels = nextChannels;
118
+ } else {
119
+ delete next.channels;
120
+ }
121
+ return next;
122
+ },
123
+ isConfigured: (_account, cfg) =>
124
+ Boolean(resolvePopoCredentials(cfg.channels?.popo as PopoConfig | undefined)),
125
+ describeAccount: (account) => ({
126
+ accountId: account.accountId,
127
+ enabled: account.enabled,
128
+ configured: account.configured,
129
+ }),
130
+ resolveAllowFrom: ({ cfg }) =>
131
+ (cfg.channels?.popo as PopoConfig | undefined)?.allowFrom ?? [],
132
+ formatAllowFrom: ({ allowFrom }) =>
133
+ allowFrom
134
+ .map((entry) => String(entry).trim())
135
+ .filter(Boolean)
136
+ .map((entry) => entry.toLowerCase()),
137
+ },
138
+ security: {
139
+ collectWarnings: ({ cfg }) => {
140
+ const popoCfg = cfg.channels?.popo as PopoConfig | undefined;
141
+ const defaultGroupPolicy = (cfg.channels as Record<string, { groupPolicy?: string }> | undefined)?.defaults?.groupPolicy;
142
+ const groupPolicy = popoCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
143
+ if (groupPolicy !== "open") return [];
144
+ return [
145
+ `- POPO groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.popo.groupPolicy="allowlist" + channels.popo.groupAllowFrom to restrict senders.`,
146
+ ];
147
+ },
148
+ },
149
+ setup: {
150
+ resolveAccountId: () => DEFAULT_ACCOUNT_ID,
151
+ applyAccountConfig: ({ cfg }) => ({
152
+ ...cfg,
153
+ channels: {
154
+ ...cfg.channels,
155
+ popo: {
156
+ ...cfg.channels?.popo,
157
+ enabled: true,
158
+ },
159
+ },
160
+ }),
161
+ },
162
+ messaging: {
163
+ normalizeTarget: normalizePopoTarget,
164
+ targetResolver: {
165
+ looksLikeId: looksLikePopoId,
166
+ hint: "<email|user:email|group:groupId>",
167
+ },
168
+ },
169
+ outbound: popoOutbound,
170
+ actions: {
171
+ listActions: ({ cfg }) => {
172
+ const enabled = cfg.channels?.popo?.enabled !== false;
173
+ if (!enabled) return [];
174
+ // Return all supported actions
175
+ // Use built-in action names to work with OpenClaw's target mode system:
176
+ // - unsend (recall), channel-create (create-team), addParticipant (invite-team)
177
+ // - channel-delete (drop-team), member-info (team-members), channel-info (team-info)
178
+ // - channel-edit (update-team-info)
179
+ return [
180
+ "send",
181
+ "card",
182
+ "unsend",
183
+ "channel-create",
184
+ "addParticipant",
185
+ "channel-delete",
186
+ "member-info",
187
+ "channel-info",
188
+ "channel-edit",
189
+ "read-ack",
190
+ "download-file",
191
+ "update-team-mgmt",
192
+ "view-scope",
193
+ "modify-view-scope",
194
+ "publish-robot",
195
+ "update-instruction-options",
196
+ "configure-card-callback",
197
+ ];
198
+ },
199
+ supportsCards: ({ cfg }) => {
200
+ return cfg.channels?.popo?.enabled !== false;
201
+ },
202
+ handleAction: async (ctx) => {
203
+ const { action, params, cfg } = ctx;
204
+
205
+ // Handle send action (with optional card)
206
+ if (action === "send") {
207
+ if (params.card) {
208
+ const card = params.card as Record<string, unknown>;
209
+ const to =
210
+ typeof params.to === "string"
211
+ ? params.to.trim()
212
+ : typeof params.target === "string"
213
+ ? params.target.trim()
214
+ : "";
215
+ if (!to) {
216
+ return {
217
+ isError: true,
218
+ content: [{ type: "text", text: "Card send requires a target (to)." }],
219
+ };
220
+ }
221
+ const { sendCardPopo } = await import("./send.js");
222
+ const result = await sendCardPopo({
223
+ cfg,
224
+ to,
225
+ templateUuid: String(card.templateUuid ?? ""),
226
+ instanceUuid: String(card.instanceUuid ?? ""),
227
+ callBackConfigKey: card.callBackConfigKey ? String(card.callBackConfigKey) : undefined,
228
+ publicVariableMap: (card.publicVariableMap as Record<string, unknown>) ?? {},
229
+ batchPrivateVariableMap: card.batchPrivateVariableMap as Record<string, Record<string, unknown>> | undefined,
230
+ options: card.options as import("./send.js").PopoCardOptions | undefined,
231
+ });
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: JSON.stringify({
237
+ ok: true,
238
+ channel: "popo",
239
+ messageId: result.messageId,
240
+ sessionId: result.sessionId,
241
+ }),
242
+ },
243
+ ],
244
+ };
245
+ }
246
+
247
+ // Regular text send
248
+ const to =
249
+ typeof params.to === "string"
250
+ ? params.to.trim()
251
+ : typeof params.target === "string"
252
+ ? params.target.trim()
253
+ : "";
254
+ const text = typeof params.text === "string" ? params.text : typeof params.message === "string" ? params.message : "";
255
+ if (!to || !text) {
256
+ return {
257
+ isError: true,
258
+ content: [{ type: "text", text: "Send requires target (to) and text/message." }],
259
+ };
260
+ }
261
+ const result = await sendMessagePopo({ cfg, to, text });
262
+ return {
263
+ content: [
264
+ {
265
+ type: "text",
266
+ text: JSON.stringify({
267
+ ok: true,
268
+ channel: "popo",
269
+ messageId: result.messageId,
270
+ sessionId: result.sessionId,
271
+ }),
272
+ },
273
+ ],
274
+ };
275
+ }
276
+
277
+ // Handle card action
278
+ if (action === "card") {
279
+ const card = params.card as Record<string, unknown>;
280
+ const to =
281
+ typeof params.to === "string"
282
+ ? params.to.trim()
283
+ : typeof params.target === "string"
284
+ ? params.target.trim()
285
+ : "";
286
+ if (!to) {
287
+ return {
288
+ isError: true,
289
+ content: [{ type: "text", text: "Card action requires a target (to)." }],
290
+ };
291
+ }
292
+ const { sendCardPopo } = await import("./send.js");
293
+ const result = await sendCardPopo({
294
+ cfg,
295
+ to,
296
+ templateUuid: String(card?.templateUuid ?? ""),
297
+ instanceUuid: String(card?.instanceUuid ?? ""),
298
+ callBackConfigKey: card?.callBackConfigKey ? String(card.callBackConfigKey) : undefined,
299
+ publicVariableMap: (card?.publicVariableMap as Record<string, unknown>) ?? {},
300
+ batchPrivateVariableMap: card?.batchPrivateVariableMap as Record<string, Record<string, unknown>> | undefined,
301
+ options: card?.options as import("./send.js").PopoCardOptions | undefined,
302
+ });
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: JSON.stringify({
308
+ ok: true,
309
+ channel: "popo",
310
+ messageId: result.messageId,
311
+ sessionId: result.sessionId,
312
+ }),
313
+ },
314
+ ],
315
+ };
316
+ }
317
+
318
+ // Handle unsend action (recall message) - uses built-in "unsend" which has proper target mode
319
+ // Accepts: target (to), messageId, sessionId, sessionType
320
+ if (action === "unsend") {
321
+ const msgId = typeof params.messageId === "string" ? params.messageId : typeof params.msgId === "string" ? params.msgId : "";
322
+ // For unsend, sessionId can come from "to" (target) or explicit sessionId
323
+ const sessionId =
324
+ typeof params.sessionId === "string"
325
+ ? params.sessionId
326
+ : typeof params.to === "string"
327
+ ? params.to.trim()
328
+ : "";
329
+ const sessionType = typeof params.sessionType === "number" ? params.sessionType : 1;
330
+ if (!msgId || !sessionId) {
331
+ return {
332
+ isError: true,
333
+ content: [{ type: "text", text: "Unsend requires messageId and target (or sessionId)." }],
334
+ };
335
+ }
336
+ const result = await recallMessagePopo({ cfg, msgId, sessionId, sessionType: sessionType as 1 | 3 });
337
+ return {
338
+ content: [
339
+ {
340
+ type: "text",
341
+ text: JSON.stringify({
342
+ ok: result.success,
343
+ channel: "popo",
344
+ error: result.error,
345
+ }),
346
+ },
347
+ ],
348
+ };
349
+ }
350
+
351
+ // Handle read-ack action
352
+ if (action === "read-ack") {
353
+ const msgId = typeof params.msgId === "string" ? params.msgId : "";
354
+ const type = typeof params.type === "number" ? params.type : 1;
355
+ const st = typeof params.st === "number" ? params.st : Date.now();
356
+ const page = typeof params.page === "number" ? params.page : 1;
357
+ const size = typeof params.size === "number" ? params.size : 30;
358
+ if (!msgId) {
359
+ return {
360
+ isError: true,
361
+ content: [{ type: "text", text: "Read-ack requires msgId." }],
362
+ };
363
+ }
364
+ const result = await getMessageReadAckPopo({
365
+ cfg,
366
+ msgId,
367
+ sessionType: 3,
368
+ type: type as 1 | 2,
369
+ st,
370
+ page,
371
+ size,
372
+ });
373
+ return {
374
+ content: [
375
+ {
376
+ type: "text",
377
+ text: JSON.stringify({
378
+ ok: result.success,
379
+ channel: "popo",
380
+ rnum: result.rnum,
381
+ unrnum: result.unrnum,
382
+ list: result.list,
383
+ error: result.error,
384
+ }),
385
+ },
386
+ ],
387
+ };
388
+ }
389
+
390
+ // Handle download-file action
391
+ if (action === "download-file") {
392
+ const fileId = typeof params.fileId === "string" ? params.fileId : "";
393
+ if (!fileId) {
394
+ return {
395
+ isError: true,
396
+ content: [{ type: "text", text: "Download-file requires fileId." }],
397
+ };
398
+ }
399
+ const result = await downloadMessageFilePopo({ cfg, fileId });
400
+ return {
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify({
405
+ ok: result.success,
406
+ channel: "popo",
407
+ downloadUrl: result.downloadUrl,
408
+ error: result.error,
409
+ }),
410
+ },
411
+ ],
412
+ };
413
+ }
414
+
415
+ // ==================== Team Management Actions ====================
416
+
417
+ // Handle channel-create action (create team)
418
+ // Built-in action with targetMode: none
419
+ if (action === "channel-create") {
420
+ const uid = typeof params.uid === "string" ? params.uid : "";
421
+ const name = typeof params.name === "string" ? params.name : undefined;
422
+ const uidList = Array.isArray(params.uidList) ? params.uidList : [];
423
+ const photoUrl = typeof params.photoUrl === "string" ? params.photoUrl : undefined;
424
+ if (!uid || uidList.length === 0) {
425
+ return {
426
+ isError: true,
427
+ content: [{ type: "text", text: "channel-create requires uid (owner) and uidList (members)." }],
428
+ };
429
+ }
430
+ const result = await createTeam({ cfg, uid, name, uidList, photoUrl });
431
+ return {
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: JSON.stringify({
436
+ ok: result.success,
437
+ channel: "popo",
438
+ tid: result.tid,
439
+ passNum: result.passNum,
440
+ failList: result.failList,
441
+ error: result.error,
442
+ }),
443
+ },
444
+ ],
445
+ };
446
+ }
447
+
448
+ // Handle addParticipant action (invite to team)
449
+ // Built-in action with targetMode: to (tid comes from target/to)
450
+ if (action === "addParticipant") {
451
+ const tid =
452
+ typeof params.tid === "string"
453
+ ? params.tid
454
+ : typeof params.to === "string"
455
+ ? params.to.trim()
456
+ : "";
457
+ const inviteList = Array.isArray(params.inviteList) ? params.inviteList : Array.isArray(params.participant) ? params.participant : [];
458
+ const type = typeof params.type === "string" ? (params.type as "1" | "2") : "1";
459
+ const text = typeof params.text === "string" ? params.text : undefined;
460
+ if (!tid || inviteList.length === 0) {
461
+ return {
462
+ isError: true,
463
+ content: [{ type: "text", text: "addParticipant requires target (tid) and inviteList/participant." }],
464
+ };
465
+ }
466
+ const result = await inviteToTeam({ cfg, tid, inviteList, type, text });
467
+ return {
468
+ content: [
469
+ {
470
+ type: "text",
471
+ text: JSON.stringify({
472
+ ok: result.success,
473
+ channel: "popo",
474
+ type: result.type,
475
+ failList: result.failList,
476
+ error: result.error,
477
+ }),
478
+ },
479
+ ],
480
+ };
481
+ }
482
+
483
+ // Handle channel-delete action (drop/disband team)
484
+ // Built-in action with targetMode: channelId
485
+ if (action === "channel-delete") {
486
+ const tid =
487
+ typeof params.tid === "string"
488
+ ? params.tid
489
+ : typeof params.channelId === "string"
490
+ ? params.channelId.trim()
491
+ : "";
492
+ if (!tid) {
493
+ return {
494
+ isError: true,
495
+ content: [{ type: "text", text: "channel-delete requires channelId (team ID)." }],
496
+ };
497
+ }
498
+ const result = await dropTeam({ cfg, tid });
499
+ return {
500
+ content: [
501
+ {
502
+ type: "text",
503
+ text: JSON.stringify({
504
+ ok: result.success,
505
+ channel: "popo",
506
+ error: result.error,
507
+ }),
508
+ },
509
+ ],
510
+ };
511
+ }
512
+
513
+ // Handle member-info action (get team members)
514
+ // Built-in action with targetMode: none
515
+ if (action === "member-info") {
516
+ const tid = typeof params.tid === "string" ? params.tid : "";
517
+ const page = typeof params.page === "number" ? params.page : 1;
518
+ const limit = typeof params.limit === "number" ? params.limit : 20;
519
+ if (!tid) {
520
+ return {
521
+ isError: true,
522
+ content: [{ type: "text", text: "member-info requires tid (team ID)." }],
523
+ };
524
+ }
525
+ const result = await getTeamMembers({ cfg, tid, page, limit });
526
+ return {
527
+ content: [
528
+ {
529
+ type: "text",
530
+ text: JSON.stringify({
531
+ ok: result.success,
532
+ channel: "popo",
533
+ records: result.records,
534
+ total: result.total,
535
+ page: result.page,
536
+ limit: result.limit,
537
+ error: result.error,
538
+ }),
539
+ },
540
+ ],
541
+ };
542
+ }
543
+
544
+ // Handle channel-info action (get team info)
545
+ // Built-in action with targetMode: channelId
546
+ if (action === "channel-info") {
547
+ const tid =
548
+ typeof params.tid === "string"
549
+ ? params.tid
550
+ : typeof params.channelId === "string"
551
+ ? params.channelId.trim()
552
+ : "";
553
+ const scopes = Array.isArray(params.scopes) ? params.scopes : undefined;
554
+ if (!tid) {
555
+ return {
556
+ isError: true,
557
+ content: [{ type: "text", text: "channel-info requires channelId (team ID)." }],
558
+ };
559
+ }
560
+ const result = await getTeamInfo({ cfg, tid, scopes });
561
+ return {
562
+ content: [
563
+ {
564
+ type: "text",
565
+ text: JSON.stringify({
566
+ ok: result.success,
567
+ channel: "popo",
568
+ info: result.info,
569
+ error: result.error,
570
+ }),
571
+ },
572
+ ],
573
+ };
574
+ }
575
+
576
+ // Handle channel-edit action (update team info)
577
+ // Built-in action with targetMode: channelId
578
+ if (action === "channel-edit") {
579
+ const tid =
580
+ typeof params.tid === "string"
581
+ ? params.tid
582
+ : typeof params.channelId === "string"
583
+ ? params.channelId.trim()
584
+ : "";
585
+ const name = typeof params.name === "string" ? params.name : "";
586
+ const board = typeof params.board === "string" ? params.board : "";
587
+ const type = typeof params.type === "number" ? params.type : undefined;
588
+ if (!tid || !name || !board) {
589
+ return {
590
+ isError: true,
591
+ content: [{ type: "text", text: "channel-edit requires channelId (tid), name, and board." }],
592
+ };
593
+ }
594
+ const result = await updateTeamInfo({ cfg, tid, name, board, type });
595
+ return {
596
+ content: [
597
+ {
598
+ type: "text",
599
+ text: JSON.stringify({
600
+ ok: result.success,
601
+ channel: "popo",
602
+ error: result.error,
603
+ }),
604
+ },
605
+ ],
606
+ };
607
+ }
608
+
609
+ // Handle update-team-mgmt action
610
+ if (action === "update-team-mgmt") {
611
+ const tid = typeof params.tid === "string" ? params.tid : "";
612
+ const settingType = typeof params.type === "number" ? params.type : typeof params.settingType === "number" ? params.settingType : 0;
613
+ const value = typeof params.value === "number" ? params.value : 0;
614
+ if (!tid || !settingType) {
615
+ return {
616
+ isError: true,
617
+ content: [{ type: "text", text: "Update-team-mgmt requires tid, type (setting type 1-8), and value." }],
618
+ };
619
+ }
620
+ const result = await updateTeamManagement({ cfg, tid, type: settingType, value });
621
+ return {
622
+ content: [
623
+ {
624
+ type: "text",
625
+ text: JSON.stringify({
626
+ ok: result.success,
627
+ channel: "popo",
628
+ error: result.error,
629
+ }),
630
+ },
631
+ ],
632
+ };
633
+ }
634
+
635
+ // ==================== View Scope Actions ====================
636
+
637
+ // Handle view-scope action
638
+ if (action === "view-scope") {
639
+ const scopeType = typeof params.scopeType === "string" ? (params.scopeType as "PERSONAL" | "DEPARTMENT") : "PERSONAL";
640
+ const current = typeof params.current === "number" ? params.current : 1;
641
+ const limit = typeof params.limit === "number" ? params.limit : 10;
642
+ const result = await getViewScope({ cfg, scopeType, current, limit });
643
+ return {
644
+ content: [
645
+ {
646
+ type: "text",
647
+ text: JSON.stringify({
648
+ ok: result.success,
649
+ channel: "popo",
650
+ records: result.records,
651
+ viewScopeType: result.viewScopeType,
652
+ total: result.total,
653
+ error: result.error,
654
+ }),
655
+ },
656
+ ],
657
+ };
658
+ }
659
+
660
+ // Handle modify-view-scope action
661
+ if (action === "modify-view-scope") {
662
+ const operationType = typeof params.operationType === "number" ? (params.operationType as 1 | 2) : 1;
663
+ const scopeType = typeof params.scopeType === "string" ? (params.scopeType as "PERSONAL" | "DEPARTMENT") : "PERSONAL";
664
+ const scopeValues = Array.isArray(params.scopeValues) ? params.scopeValues : [];
665
+ if (scopeValues.length === 0) {
666
+ return {
667
+ isError: true,
668
+ content: [{ type: "text", text: "Modify-view-scope requires scopeValues array." }],
669
+ };
670
+ }
671
+ const result = await modifyViewScope({ cfg, operationType, scopeType, scopeValues });
672
+ return {
673
+ content: [
674
+ {
675
+ type: "text",
676
+ text: JSON.stringify({
677
+ ok: result.success,
678
+ channel: "popo",
679
+ successNum: result.successNum,
680
+ failNum: result.failNum,
681
+ failMsg: result.failMsg,
682
+ error: result.error,
683
+ }),
684
+ },
685
+ ],
686
+ };
687
+ }
688
+
689
+ // Handle publish-robot action
690
+ if (action === "publish-robot") {
691
+ const uid = typeof params.uid === "string" ? params.uid : "";
692
+ if (!uid) {
693
+ return {
694
+ isError: true,
695
+ content: [{ type: "text", text: "Publish-robot requires uid (applicant email)." }],
696
+ };
697
+ }
698
+ const result = await publishRobot({ cfg, uid });
699
+ return {
700
+ content: [
701
+ {
702
+ type: "text",
703
+ text: JSON.stringify({
704
+ ok: result.success,
705
+ channel: "popo",
706
+ error: result.error,
707
+ }),
708
+ },
709
+ ],
710
+ };
711
+ }
712
+
713
+ // ==================== Instruction Variable Options ====================
714
+
715
+ // Handle update-instruction-options action
716
+ if (action === "update-instruction-options") {
717
+ const instructionId = typeof params.instructionId === "string" ? params.instructionId : "";
718
+ const variableKey = typeof params.variableKey === "string" ? params.variableKey : "";
719
+ const variableOptions = Array.isArray(params.variableOptions) ? params.variableOptions : [];
720
+ if (!instructionId || !variableKey || variableOptions.length === 0) {
721
+ return {
722
+ isError: true,
723
+ content: [{ type: "text", text: "Update-instruction-options requires instructionId, variableKey, and variableOptions." }],
724
+ };
725
+ }
726
+ const result = await updateInstructionVariableOptions({ cfg, instructionId, variableKey, variableOptions });
727
+ return {
728
+ content: [
729
+ {
730
+ type: "text",
731
+ text: JSON.stringify({
732
+ ok: result.success,
733
+ channel: "popo",
734
+ traceId: result.traceId,
735
+ error: result.error,
736
+ }),
737
+ },
738
+ ],
739
+ };
740
+ }
741
+
742
+ // ==================== Card Callback Configuration ====================
743
+
744
+ // Handle configure-card-callback action
745
+ if (action === "configure-card-callback") {
746
+ const configKey = typeof params.configKey === "string" ? params.configKey : "";
747
+ const requestUrl = typeof params.requestUrl === "string" ? params.requestUrl : "";
748
+ const token = typeof params.token === "string" ? params.token : "";
749
+ if (!configKey || !requestUrl || !token) {
750
+ return {
751
+ isError: true,
752
+ content: [{ type: "text", text: "Configure-card-callback requires configKey, requestUrl, and token." }],
753
+ };
754
+ }
755
+ const aesKey = typeof params.aesKey === "string" ? params.aesKey : undefined;
756
+ const overwriteUpdate = typeof params.overwriteUpdate === "boolean" ? params.overwriteUpdate : false;
757
+ const withCallBackTest = typeof params.withCallBackTest === "boolean" ? params.withCallBackTest : true;
758
+ const pushToOffice = typeof params.pushToOffice === "boolean" ? params.pushToOffice : false;
759
+ const authType = typeof params.authType === "string" ? (params.authType as "CODE" | "SIGN") : "SIGN";
760
+ const result = await configureCardCallbackPopo({
761
+ cfg,
762
+ configKey,
763
+ requestUrl,
764
+ token,
765
+ aesKey,
766
+ overwriteUpdate,
767
+ withCallBackTest,
768
+ pushToOffice,
769
+ authType,
770
+ });
771
+ return {
772
+ content: [
773
+ {
774
+ type: "text",
775
+ text: JSON.stringify({
776
+ ok: result.success,
777
+ channel: "popo",
778
+ config: result.config,
779
+ error: result.error,
780
+ }),
781
+ },
782
+ ],
783
+ };
784
+ }
785
+
786
+ return undefined;
787
+ },
788
+ },
789
+ status: {
790
+ defaultRuntime: {
791
+ accountId: DEFAULT_ACCOUNT_ID,
792
+ running: false,
793
+ lastStartAt: null,
794
+ lastStopAt: null,
795
+ lastError: null,
796
+ port: null,
797
+ },
798
+ buildChannelSummary: ({ snapshot, cfg }) => ({
799
+ configured: snapshot.configured ?? false,
800
+ running: snapshot.running ?? false,
801
+ lastStartAt: snapshot.lastStartAt ?? null,
802
+ lastStopAt: snapshot.lastStopAt ?? null,
803
+ lastError: snapshot.lastError ?? null,
804
+ port: snapshot.port ?? null,
805
+ probe: snapshot.probe,
806
+ lastProbeAt: snapshot.lastProbeAt ?? null,
807
+ webhookPath: (cfg.channels?.popo as PopoConfig)?.webhookPath ?? '/popo/events',
808
+ }),
809
+ probeAccount: async ({ cfg }) =>
810
+ await probePopo(cfg.channels?.popo as PopoConfig | undefined),
811
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
812
+ accountId: account.accountId,
813
+ enabled: account.enabled,
814
+ configured: account.configured,
815
+ running: runtime?.running ?? false,
816
+ lastStartAt: runtime?.lastStartAt ?? null,
817
+ lastStopAt: runtime?.lastStopAt ?? null,
818
+ lastError: runtime?.lastError ?? null,
819
+ port: runtime?.port ?? null,
820
+ probe,
821
+ }),
822
+ },
823
+ gateway: {
824
+ startAccount: async (ctx) => {
825
+ const { monitorPopoProvider } = await import("./monitor.js");
826
+ const popoCfg = ctx.cfg.channels?.popo as PopoConfig | undefined;
827
+ const webhookPath = popoCfg?.webhookPath ?? "/popo/events";
828
+ // Set port to null to indicate gateway mode
829
+ ctx.setStatus({ accountId: ctx.accountId, port: null });
830
+ ctx.log?.info(`starting popo provider (gateway mode, path: ${webhookPath})`);
831
+ return monitorPopoProvider({
832
+ config: ctx.cfg,
833
+ runtime: ctx.runtime,
834
+ abortSignal: ctx.abortSignal,
835
+ accountId: ctx.accountId,
836
+ });
837
+ },
838
+ },
839
+ };