@xopcai/xopc 0.0.48 → 0.0.49
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/dist/extensions/dingtalk/src/accounts.js +1 -1
- package/dist/extensions/dingtalk/src/accounts.js.map +1 -1
- package/dist/extensions/dingtalk/src/merge-config.js +1 -1
- package/dist/extensions/dingtalk/src/merge-config.js.map +1 -1
- package/dist/extensions/dingtalk/src/plugin.js +1 -1
- package/dist/extensions/dingtalk/src/plugin.js.map +1 -1
- package/dist/extensions/feishu/src/adapters/cli-login.js +8 -8
- package/dist/extensions/feishu/src/adapters/cli-login.js.map +1 -1
- package/dist/extensions/feishu/src/adapters/onboard-cli.js +8 -8
- package/dist/extensions/feishu/src/adapters/onboard-cli.js.map +1 -1
- package/dist/extensions/feishu/src/plugin.js +1 -1
- package/dist/extensions/feishu/src/plugin.js.map +1 -1
- package/dist/extensions/feishu/src/schema/config-schema.js +2 -2
- package/dist/extensions/feishu/src/schema/config-schema.js.map +1 -1
- package/dist/extensions/feishu/src/state/accounts.js +1 -1
- package/dist/extensions/feishu/src/state/accounts.js.map +1 -1
- package/dist/extensions/feishu/src/streaming/streaming-adapter.js +82 -36
- package/dist/extensions/feishu/src/streaming/streaming-adapter.js.map +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/auth/accounts.js +1 -1
- package/dist/extensions/weixin/src/auth/accounts.js.map +1 -1
- package/dist/extensions/weixin/src/config-schema.js +2 -2
- package/dist/extensions/weixin/src/config-schema.js.map +1 -1
- package/dist/extensions/weixin/src/config-surface.js +1 -1
- package/dist/extensions/weixin/src/config-surface.js.map +1 -1
- package/dist/extensions/weixin/src/messaging/process-message.js +2 -2
- package/dist/extensions/weixin/src/messaging/process-message.js.map +1 -1
- package/dist/extensions/weixin/src/plugin.js +1 -1
- package/dist/extensions/weixin/src/plugin.js.map +1 -1
- package/dist/gateway/static/root/assets/{agents-BdISC5UA.js → agents-CQllyJhj.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-BdISC5UA.js.map → agents-CQllyJhj.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-CXU_Jg95.js → apps-page-BWI3RVMh.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-CXU_Jg95.js.map → apps-page-BWI3RVMh.js.map} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-rfmhXfC4.js +2 -0
- package/dist/gateway/static/root/assets/channels-settings-rfmhXfC4.js.map +1 -0
- package/dist/gateway/static/root/assets/{cron-dreaming-jobs-C-V4_3vz.js → cron-dreaming-jobs-BzCQy56Z.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-dreaming-jobs-C-V4_3vz.js.map → cron-dreaming-jobs-BzCQy56Z.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-page-CIIy81K6.js → cron-page-CjTuH_av.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-CIIy81K6.js.map → cron-page-CjTuH_av.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-VW7dXc5X.js → dist-DIeOihYP.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-VW7dXc5X.js.map → dist-DIeOihYP.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-Cslwx62-.js → extension-debug-page-CR-4lZOn.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-Cslwx62-.js.map → extension-debug-page-CR-4lZOn.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-Dzyebr6T.js → extension-page-BNwcYj2Q.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-Dzyebr6T.js.map → extension-page-BNwcYj2Q.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-B07uetL_.js → extension-settings-page-CDpiZPV4.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-B07uetL_.js.map → extension-settings-page-CDpiZPV4.js.map} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-config-api-kmvGNriW.js → heartbeat-config-api-CSbqK-L_.js} +2 -2
- package/dist/gateway/static/root/assets/{heartbeat-config-api-kmvGNriW.js.map → heartbeat-config-api-CSbqK-L_.js.map} +1 -1
- package/dist/gateway/static/root/assets/{index-DW6JvymK.js → index-D3sMd_aw.js} +4 -4
- package/dist/gateway/static/root/assets/{index-DW6JvymK.js.map → index-D3sMd_aw.js.map} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-BiRnV2Ex.js → logs-page-DLcWAXU5.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-BiRnV2Ex.js.map → logs-page-DLcWAXU5.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-DQq0QaQh.js → sessions-page-Ck3lS3N_.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-DQq0QaQh.js.map → sessions-page-Ck3lS3N_.js.map} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-BV_l8nJ3.js → settings-page-Dp_eFgub.js} +2 -2
- package/dist/gateway/static/root/assets/{settings-page-BV_l8nJ3.js.map → settings-page-Dp_eFgub.js.map} +1 -1
- package/dist/gateway/static/root/assets/{skills-page-Bv0pphDM.js → skills-page-ClRj9e6K.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-Bv0pphDM.js.map → skills-page-ClRj9e6K.js.map} +1 -1
- package/dist/gateway/static/root/assets/{use-image-provider-credentials-BPcW1K0N.js → use-image-provider-credentials-DAi5Iu8c.js} +2 -2
- package/dist/gateway/static/root/assets/{use-image-provider-credentials-BPcW1K0N.js.map → use-image-provider-credentials-DAi5Iu8c.js.map} +1 -1
- package/dist/gateway/static/root/index.html +1 -1
- package/dist/package.js +1 -1
- package/dist/src/agent/service.js +2 -0
- package/dist/src/agent/service.js.map +1 -1
- package/dist/src/gateway/hono/routes/channels.js +2 -2
- package/dist/src/gateway/hono/routes/channels.js.map +1 -1
- package/dist/src/gateway/hono/routes/config.js +3 -3
- package/dist/src/gateway/hono/routes/config.js.map +1 -1
- package/package.json +1 -1
- package/dist/gateway/static/root/assets/channels-settings-Doe1ciOW.js +0 -2
- package/dist/gateway/static/root/assets/channels-settings-Doe1ciOW.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","names":[],"sources":["../../../../../extensions/feishu/src/state/accounts.ts"],"sourcesContent":["import type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport type { FeishuAccountConfig, FeishuConfig } from '../schema/config-schema.js';\n\nexport interface ResolvedFeishuAccount {\n accountId: string;\n name?: string;\n enabled: boolean;\n configured: boolean;\n\n appId?: string;\n appSecret?: string;\n domain: 'feishu' | 'lark' | string;\n connectionMode: 'websocket' | 'webhook';\n\n webhookHost?: string;\n webhookPort?: number;\n webhookPath?: string;\n verificationToken?: string;\n encryptKey?: string;\n\n dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';\n groupPolicy: 'open' | 'disabled' | 'allowlist';\n allowFrom?: Array<string | number>;\n groupAllowFrom?: Array<string | number>;\n requireMention?: boolean;\n\n historyLimit: number;\n textChunkLimit: number;\n renderMode?: 'auto' | 'raw' | 'card';\n\n reactionNotifications?: 'off' | 'own' | 'all';\n\n /** Opt-in: only `true` enables Feishu streaming (Thinking… + incremental updates). */\n streaming: boolean;\n blockStreamingCoalesce?: { enabled?: boolean; minChars?: number; idleMs?: number };\n\n tools?: FeishuConfig['tools'];\n actions?: FeishuConfig['actions'];\n dynamicAgentCreation?: FeishuConfig['dynamicAgentCreation'];\n}\n\nfunction asFeishuSection(cfg: Config): FeishuConfig | undefined {\n return cfg.channels?.feishu as FeishuConfig | undefined;\n}\n\nexport function listFeishuAccountIds(cfg: Config): string[] {\n const section = asFeishuSection(cfg);\n if (!section) return [];\n const accounts = section.accounts ?? {};\n const keys = Object.keys(accounts);\n if (keys.length > 0) return keys;\n // Single-account layout\n return ['default'];\n}\n\nfunction resolveRootAccount(section: FeishuConfig): FeishuAccountConfig {\n return {\n enabled: section.enabled,\n name: 'Default Account',\n appId: section.appId,\n appSecret: section.appSecret,\n domain: section.domain,\n connectionMode: section.connectionMode,\n webhookHost: (section as any).webhookHost,\n webhookPort: (section as any).webhookPort,\n webhookPath: (section as any).webhookPath,\n verificationToken: (section as any).verificationToken,\n encryptKey: (section as any).encryptKey,\n dmPolicy: section.dmPolicy,\n groupPolicy: section.groupPolicy,\n allowFrom: section.allowFrom,\n groupAllowFrom: section.groupAllowFrom,\n requireMention: section.requireMention,\n historyLimit: section.historyLimit,\n textChunkLimit: section.textChunkLimit,\n renderMode: (section as any).renderMode,\n reactionNotifications: section.reactionNotifications,\n streaming: section.streaming,\n blockStreamingCoalesce: section.blockStreamingCoalesce,\n tools: section.tools,\n actions: section.actions,\n dynamicAgentCreation: section.dynamicAgentCreation,\n };\n}\n\nfunction mergeAccount(section: FeishuConfig, account: FeishuAccountConfig | undefined): FeishuAccountConfig {\n const root = resolveRootAccount(section);\n if (!account) return root;\n return {\n ...root,\n ...account,\n // arrays should replace, not concat\n allowFrom: account.allowFrom ?? root.allowFrom,\n groupAllowFrom: account.groupAllowFrom ?? root.groupAllowFrom,\n };\n}\n\nexport function resolveFeishuAccount(cfg: Config, accountId?: string | null): ResolvedFeishuAccount {\n const section = asFeishuSection(cfg) ?? ({ enabled: false } as FeishuConfig);\n const requested = (accountId ?? '').trim();\n\n const accounts = section.accounts ?? {};\n const hasNamedAccounts = Object.keys(accounts).length > 0;\n const effectiveAccountId = requested || section.defaultAccount || (hasNamedAccounts ? Object.keys(accounts)[0] : 'default');\n\n const raw = hasNamedAccounts ? accounts[effectiveAccountId] : resolveRootAccount(section);\n const merged = mergeAccount(section, raw);\n\n const enabled = merged.enabled !== false && section.enabled !== false;\n const appId = merged.appId?.trim() || undefined;\n const appSecret = merged.appSecret?.trim() || undefined;\n const configured = Boolean(appId && appSecret);\n\n return {\n accountId: effectiveAccountId,\n name: merged.name,\n enabled,\n configured,\n appId,\n appSecret,\n domain: (merged.domain ?? 'feishu') as any,\n connectionMode: (merged.connectionMode ?? 'websocket') as any,\n webhookHost: (merged as any).webhookHost,\n webhookPort: (merged as any).webhookPort,\n webhookPath: (merged as any).webhookPath,\n verificationToken: (merged as any).verificationToken,\n encryptKey: (merged as any).encryptKey,\n dmPolicy: (merged.dmPolicy ?? '
|
|
1
|
+
{"version":3,"file":"accounts.js","names":[],"sources":["../../../../../extensions/feishu/src/state/accounts.ts"],"sourcesContent":["import type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport type { FeishuAccountConfig, FeishuConfig } from '../schema/config-schema.js';\n\nexport interface ResolvedFeishuAccount {\n accountId: string;\n name?: string;\n enabled: boolean;\n configured: boolean;\n\n appId?: string;\n appSecret?: string;\n domain: 'feishu' | 'lark' | string;\n connectionMode: 'websocket' | 'webhook';\n\n webhookHost?: string;\n webhookPort?: number;\n webhookPath?: string;\n verificationToken?: string;\n encryptKey?: string;\n\n dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';\n groupPolicy: 'open' | 'disabled' | 'allowlist';\n allowFrom?: Array<string | number>;\n groupAllowFrom?: Array<string | number>;\n requireMention?: boolean;\n\n historyLimit: number;\n textChunkLimit: number;\n renderMode?: 'auto' | 'raw' | 'card';\n\n reactionNotifications?: 'off' | 'own' | 'all';\n\n /** Opt-in: only `true` enables Feishu streaming (Thinking… + incremental updates). */\n streaming: boolean;\n blockStreamingCoalesce?: { enabled?: boolean; minChars?: number; idleMs?: number };\n\n tools?: FeishuConfig['tools'];\n actions?: FeishuConfig['actions'];\n dynamicAgentCreation?: FeishuConfig['dynamicAgentCreation'];\n}\n\nfunction asFeishuSection(cfg: Config): FeishuConfig | undefined {\n return cfg.channels?.feishu as FeishuConfig | undefined;\n}\n\nexport function listFeishuAccountIds(cfg: Config): string[] {\n const section = asFeishuSection(cfg);\n if (!section) return [];\n const accounts = section.accounts ?? {};\n const keys = Object.keys(accounts);\n if (keys.length > 0) return keys;\n // Single-account layout\n return ['default'];\n}\n\nfunction resolveRootAccount(section: FeishuConfig): FeishuAccountConfig {\n return {\n enabled: section.enabled,\n name: 'Default Account',\n appId: section.appId,\n appSecret: section.appSecret,\n domain: section.domain,\n connectionMode: section.connectionMode,\n webhookHost: (section as any).webhookHost,\n webhookPort: (section as any).webhookPort,\n webhookPath: (section as any).webhookPath,\n verificationToken: (section as any).verificationToken,\n encryptKey: (section as any).encryptKey,\n dmPolicy: section.dmPolicy,\n groupPolicy: section.groupPolicy,\n allowFrom: section.allowFrom,\n groupAllowFrom: section.groupAllowFrom,\n requireMention: section.requireMention,\n historyLimit: section.historyLimit,\n textChunkLimit: section.textChunkLimit,\n renderMode: (section as any).renderMode,\n reactionNotifications: section.reactionNotifications,\n streaming: section.streaming,\n blockStreamingCoalesce: section.blockStreamingCoalesce,\n tools: section.tools,\n actions: section.actions,\n dynamicAgentCreation: section.dynamicAgentCreation,\n };\n}\n\nfunction mergeAccount(section: FeishuConfig, account: FeishuAccountConfig | undefined): FeishuAccountConfig {\n const root = resolveRootAccount(section);\n if (!account) return root;\n return {\n ...root,\n ...account,\n // arrays should replace, not concat\n allowFrom: account.allowFrom ?? root.allowFrom,\n groupAllowFrom: account.groupAllowFrom ?? root.groupAllowFrom,\n };\n}\n\nexport function resolveFeishuAccount(cfg: Config, accountId?: string | null): ResolvedFeishuAccount {\n const section = asFeishuSection(cfg) ?? ({ enabled: false } as FeishuConfig);\n const requested = (accountId ?? '').trim();\n\n const accounts = section.accounts ?? {};\n const hasNamedAccounts = Object.keys(accounts).length > 0;\n const effectiveAccountId = requested || section.defaultAccount || (hasNamedAccounts ? Object.keys(accounts)[0] : 'default');\n\n const raw = hasNamedAccounts ? accounts[effectiveAccountId] : resolveRootAccount(section);\n const merged = mergeAccount(section, raw);\n\n const enabled = merged.enabled !== false && section.enabled !== false;\n const appId = merged.appId?.trim() || undefined;\n const appSecret = merged.appSecret?.trim() || undefined;\n const configured = Boolean(appId && appSecret);\n\n return {\n accountId: effectiveAccountId,\n name: merged.name,\n enabled,\n configured,\n appId,\n appSecret,\n domain: (merged.domain ?? 'feishu') as any,\n connectionMode: (merged.connectionMode ?? 'websocket') as any,\n webhookHost: (merged as any).webhookHost,\n webhookPort: (merged as any).webhookPort,\n webhookPath: (merged as any).webhookPath,\n verificationToken: (merged as any).verificationToken,\n encryptKey: (merged as any).encryptKey,\n dmPolicy: (merged.dmPolicy ?? 'open') as any,\n groupPolicy: (merged.groupPolicy ?? 'allowlist') as any,\n allowFrom: merged.allowFrom,\n groupAllowFrom: merged.groupAllowFrom,\n requireMention: merged.requireMention,\n historyLimit: typeof merged.historyLimit === 'number' ? merged.historyLimit : 50,\n textChunkLimit: typeof merged.textChunkLimit === 'number' ? merged.textChunkLimit : 4000,\n renderMode: (merged as any).renderMode,\n reactionNotifications: merged.reactionNotifications,\n streaming: merged.streaming === true,\n blockStreamingCoalesce: merged.blockStreamingCoalesce as any,\n tools: merged.tools,\n actions: merged.actions,\n dynamicAgentCreation: merged.dynamicAgentCreation,\n };\n}\n\n"],"mappings":";AA0CA,SAAS,gBAAgB,KAAuC;AAC9D,QAAO,IAAI,UAAU;;AAGvB,SAAgB,qBAAqB,KAAuB;CAC1D,MAAM,UAAU,gBAAgB,IAAI;AACpC,KAAI,CAAC,QAAS,QAAO,EAAE;CACvB,MAAM,WAAW,QAAQ,YAAY,EAAE;CACvC,MAAM,OAAO,OAAO,KAAK,SAAS;AAClC,KAAI,KAAK,SAAS,EAAG,QAAO;AAE5B,QAAO,CAAC,UAAU;;AAGpB,SAAS,mBAAmB,SAA4C;AACtE,QAAO;EACL,SAAS,QAAQ;EACjB,MAAM;EACN,OAAO,QAAQ;EACf,WAAW,QAAQ;EACnB,QAAQ,QAAQ;EAChB,gBAAgB,QAAQ;EACxB,aAAc,QAAgB;EAC9B,aAAc,QAAgB;EAC9B,aAAc,QAAgB;EAC9B,mBAAoB,QAAgB;EACpC,YAAa,QAAgB;EAC7B,UAAU,QAAQ;EAClB,aAAa,QAAQ;EACrB,WAAW,QAAQ;EACnB,gBAAgB,QAAQ;EACxB,gBAAgB,QAAQ;EACxB,cAAc,QAAQ;EACtB,gBAAgB,QAAQ;EACxB,YAAa,QAAgB;EAC7B,uBAAuB,QAAQ;EAC/B,WAAW,QAAQ;EACnB,wBAAwB,QAAQ;EAChC,OAAO,QAAQ;EACf,SAAS,QAAQ;EACjB,sBAAsB,QAAQ;EAC/B;;AAGH,SAAS,aAAa,SAAuB,SAA+D;CAC1G,MAAM,OAAO,mBAAmB,QAAQ;AACxC,KAAI,CAAC,QAAS,QAAO;AACrB,QAAO;EACL,GAAG;EACH,GAAG;EAEH,WAAW,QAAQ,aAAa,KAAK;EACrC,gBAAgB,QAAQ,kBAAkB,KAAK;EAChD;;AAGH,SAAgB,qBAAqB,KAAa,WAAkD;CAClG,MAAM,UAAU,gBAAgB,IAAI,IAAK,EAAE,SAAS,OAAO;CAC3D,MAAM,aAAa,aAAa,IAAI,MAAM;CAE1C,MAAM,WAAW,QAAQ,YAAY,EAAE;CACvC,MAAM,mBAAmB,OAAO,KAAK,SAAS,CAAC,SAAS;CACxD,MAAM,qBAAqB,aAAa,QAAQ,mBAAmB,mBAAmB,OAAO,KAAK,SAAS,CAAC,KAAK;CAGjH,MAAM,SAAS,aAAa,SADhB,mBAAmB,SAAS,sBAAsB,mBAAmB,QAAQ,CAChD;CAEzC,MAAM,UAAU,OAAO,YAAY,SAAS,QAAQ,YAAY;CAChE,MAAM,QAAQ,OAAO,OAAO,MAAM,IAAI,KAAA;CACtC,MAAM,YAAY,OAAO,WAAW,MAAM,IAAI,KAAA;CAC9C,MAAM,aAAa,QAAQ,SAAS,UAAU;AAE9C,QAAO;EACL,WAAW;EACX,MAAM,OAAO;EACb;EACA;EACA;EACA;EACA,QAAS,OAAO,UAAU;EAC1B,gBAAiB,OAAO,kBAAkB;EAC1C,aAAc,OAAe;EAC7B,aAAc,OAAe;EAC7B,aAAc,OAAe;EAC7B,mBAAoB,OAAe;EACnC,YAAa,OAAe;EAC5B,UAAW,OAAO,YAAY;EAC9B,aAAc,OAAO,eAAe;EACpC,WAAW,OAAO;EAClB,gBAAgB,OAAO;EACvB,gBAAgB,OAAO;EACvB,cAAc,OAAO,OAAO,iBAAiB,WAAW,OAAO,eAAe;EAC9E,gBAAgB,OAAO,OAAO,mBAAmB,WAAW,OAAO,iBAAiB;EACpF,YAAa,OAAe;EAC5B,uBAAuB,OAAO;EAC9B,WAAW,OAAO,cAAc;EAChC,wBAAwB,OAAO;EAC/B,OAAO,OAAO;EACd,SAAS,OAAO;EAChB,sBAAsB,OAAO;EAC9B"}
|
|
@@ -4,9 +4,33 @@ import { resolveFeishuAccount } from "../state/accounts.js";
|
|
|
4
4
|
import { createFeishuClient } from "../transport/client/client.js";
|
|
5
5
|
import { getFeishuBindingByMessageId, recordFeishuMessageBinding } from "../state/message-bindings.js";
|
|
6
6
|
import { formatFeishuOutboundText } from "../format.js";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
7
8
|
//#region extensions/feishu/src/streaming/streaming-adapter.ts
|
|
8
9
|
init_logger();
|
|
9
10
|
const log = createLogger("FeishuStreaming");
|
|
11
|
+
/** Normalize CardKit `card.create` responses (SDK / OpenAPI envelope shapes differ). */
|
|
12
|
+
function extractFeishuCardKitCreateCardId(raw) {
|
|
13
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
14
|
+
const o = raw;
|
|
15
|
+
const tryId = (v) => {
|
|
16
|
+
if (typeof v !== "string") return void 0;
|
|
17
|
+
const t = v.trim();
|
|
18
|
+
return t.length > 0 ? t : void 0;
|
|
19
|
+
};
|
|
20
|
+
const fromData = (data) => {
|
|
21
|
+
if (!data || typeof data !== "object") return void 0;
|
|
22
|
+
const d = data;
|
|
23
|
+
return tryId(d.card_id) ?? tryId(d.cardId);
|
|
24
|
+
};
|
|
25
|
+
return fromData(o.data) ?? (o.data && typeof o.data === "object" ? fromData(o.data.data) : void 0) ?? tryId(o.card_id);
|
|
26
|
+
}
|
|
27
|
+
function feishuOpenApiOk(res) {
|
|
28
|
+
if (res === null || res === void 0) return false;
|
|
29
|
+
if (typeof res !== "object") return true;
|
|
30
|
+
const code = res.code;
|
|
31
|
+
if (code === void 0 || code === null) return true;
|
|
32
|
+
return Number(code) === 0;
|
|
33
|
+
}
|
|
10
34
|
function createFeishuStreamingAdapter(getConfig) {
|
|
11
35
|
return { startStream(options) {
|
|
12
36
|
const account = resolveFeishuAccount(getConfig(), options.accountId ?? "default");
|
|
@@ -21,7 +45,7 @@ function createFeishuStreamingAdapter(getConfig) {
|
|
|
21
45
|
let ready = null;
|
|
22
46
|
let cardId;
|
|
23
47
|
let cardSeq = 0;
|
|
24
|
-
const cardElementId = "
|
|
48
|
+
const cardElementId = `md_${randomUUID().replace(/-/g, "")}`;
|
|
25
49
|
let fallbackSent = false;
|
|
26
50
|
const renderMode = account.renderMode ?? "auto";
|
|
27
51
|
const preferCard = renderMode === "card" || renderMode === "auto";
|
|
@@ -119,7 +143,7 @@ function createFeishuStreamingAdapter(getConfig) {
|
|
|
119
143
|
messageId,
|
|
120
144
|
mode: preferCard ? "card" : "text"
|
|
121
145
|
}, "Feishu streaming edit failed");
|
|
122
|
-
|
|
146
|
+
await sendFallbackText(text);
|
|
123
147
|
}
|
|
124
148
|
};
|
|
125
149
|
const start = async () => {
|
|
@@ -139,41 +163,63 @@ function createFeishuStreamingAdapter(getConfig) {
|
|
|
139
163
|
content: thinking
|
|
140
164
|
}] }
|
|
141
165
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
data:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
params: { receive_id_type },
|
|
159
|
-
data: {
|
|
160
|
-
receive_id: options.chatId,
|
|
161
|
-
msg_type: "interactive",
|
|
162
|
-
content: JSON.stringify({
|
|
163
|
-
type: "card",
|
|
164
|
-
data: { card_id: cardId }
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
messageId = res?.data?.message_id ?? res?.message_id ?? void 0;
|
|
169
|
-
if (messageId) recordBindingIfReply(messageId);
|
|
170
|
-
log.info({
|
|
166
|
+
let created;
|
|
167
|
+
try {
|
|
168
|
+
created = await api.cardkit.v1.card.create({ data: {
|
|
169
|
+
type: "card_json",
|
|
170
|
+
data: JSON.stringify(cardSpec)
|
|
171
|
+
} });
|
|
172
|
+
} catch (err) {
|
|
173
|
+
log.warn({
|
|
174
|
+
err,
|
|
175
|
+
accountId: account.accountId
|
|
176
|
+
}, "Feishu cardkit.card.create threw; falling back to text stream");
|
|
177
|
+
created = null;
|
|
178
|
+
}
|
|
179
|
+
const apiOk = feishuOpenApiOk(created);
|
|
180
|
+
cardId = apiOk ? extractFeishuCardKitCreateCardId(created) : void 0;
|
|
181
|
+
if (!apiOk) log.warn({
|
|
171
182
|
accountId: account.accountId,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
code: created?.code,
|
|
184
|
+
msg: created?.msg
|
|
185
|
+
}, "Feishu cardkit.card.create returned non-zero code; falling back to text stream");
|
|
186
|
+
else if (!cardId) log.warn({
|
|
187
|
+
accountId: account.accountId,
|
|
188
|
+
responsePreview: JSON.stringify(created).slice(0, 400)
|
|
189
|
+
}, "Feishu cardkit.card.create returned no card_id; falling back to text stream");
|
|
190
|
+
if (cardId) {
|
|
191
|
+
const res = options.replyToMessageId ? await api.im.message.reply({
|
|
192
|
+
path: { message_id: options.replyToMessageId },
|
|
193
|
+
data: {
|
|
194
|
+
msg_type: "interactive",
|
|
195
|
+
content: JSON.stringify({
|
|
196
|
+
type: "card",
|
|
197
|
+
data: { card_id: cardId }
|
|
198
|
+
}),
|
|
199
|
+
...options.threadId ? { reply_in_thread: true } : {}
|
|
200
|
+
}
|
|
201
|
+
}) : await api.im.message.create({
|
|
202
|
+
params: { receive_id_type },
|
|
203
|
+
data: {
|
|
204
|
+
receive_id: options.chatId,
|
|
205
|
+
msg_type: "interactive",
|
|
206
|
+
content: JSON.stringify({
|
|
207
|
+
type: "card",
|
|
208
|
+
data: { card_id: cardId }
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
messageId = res?.data?.message_id ?? res?.message_id ?? void 0;
|
|
213
|
+
if (messageId) recordBindingIfReply(messageId);
|
|
214
|
+
log.info({
|
|
215
|
+
accountId: account.accountId,
|
|
216
|
+
chatId: options.chatId,
|
|
217
|
+
messageId,
|
|
218
|
+
mode: "card"
|
|
219
|
+
}, "Feishu streaming started");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
cardId = void 0;
|
|
177
223
|
}
|
|
178
224
|
const res = options.replyToMessageId ? await api.im.message.reply({
|
|
179
225
|
path: { message_id: options.replyToMessageId },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streaming-adapter.js","names":[],"sources":["../../../../../extensions/feishu/src/streaming/streaming-adapter.ts"],"sourcesContent":["import type {\n ChannelStreamHandle,\n ChannelStreamingAdapter,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport { createLogger } from '@xopcai/xopc/utils/logger.js';\n\nimport { resolveFeishuAccount } from '../state/accounts.js';\nimport { formatFeishuOutboundText } from '../format.js';\nimport { getFeishuBindingByMessageId, recordFeishuMessageBinding } from '../state/message-bindings.js';\nimport { createFeishuClient } from '../transport/client/client.js';\n\nconst log = createLogger('FeishuStreaming');\n\nexport function createFeishuStreamingAdapter(getConfig: () => Config): ChannelStreamingAdapter {\n return {\n startStream(options: {\n chatId: string;\n accountId?: string;\n threadId?: string;\n replyToMessageId?: string;\n parseMode?: 'Markdown' | 'HTML';\n }): ChannelStreamHandle | null {\n const cfg = getConfig();\n const account = resolveFeishuAccount(cfg, options.accountId ?? 'default');\n if (!account.configured) {\n return null;\n }\n // Feishu streaming is opt-in. When omitted/false, fall back to normal final outbound.\n if (account.streaming !== true) {\n return null;\n }\n const { api } = createFeishuClient(account);\n\n let messageId: string | undefined;\n let lastText = '';\n let timer: NodeJS.Timeout | null = null;\n let aborted = false;\n let editedAtLeastOnce = false;\n let ready: Promise<void> | null = null;\n let cardId: string | undefined;\n let cardSeq = 0;\n const cardElementId = 'md_1';\n let fallbackSent = false;\n\n const renderMode = account.renderMode ?? 'auto';\n const preferCard = renderMode === 'card' || renderMode === 'auto';\n\n const formatStreamText = (text: string) => {\n if (!text.trim()) return text;\n if (text.trim() === 'Thinking…') return text;\n const forCardMarkdown = Boolean(preferCard && cardId);\n return formatFeishuOutboundText({\n text,\n renderMode,\n forCardMarkdown,\n });\n };\n\n const recordBindingIfReply = (childMessageId: string) => {\n if (!childMessageId) return;\n const parentId = options.replyToMessageId;\n if (!parentId) return;\n const parent = getFeishuBindingByMessageId(parentId);\n if (!parent) return;\n recordFeishuMessageBinding({ ...parent, messageId: childMessageId });\n };\n\n const edit = async (text: string) => {\n if (ready) await ready;\n if (!messageId) return;\n const outbound = formatStreamText(text);\n if (cardId && preferCard) {\n cardSeq += 1;\n await (api as any).cardkit.v1.cardElement.content({\n path: { card_id: cardId, element_id: cardElementId },\n data: {\n content: outbound,\n sequence: cardSeq,\n uuid: `${cardId}:${cardSeq}`,\n },\n });\n } else {\n await (api as any).im.v1.message.update({\n path: { message_id: messageId },\n data: { msg_type: 'text', content: JSON.stringify({ text: outbound }) },\n });\n }\n editedAtLeastOnce = true;\n };\n\n const sendFallbackText = async (text: string) => {\n if (fallbackSent) return;\n fallbackSent = true;\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n try {\n const outbound = formatStreamText(text);\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n },\n });\n const mid = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (mid) recordBindingIfReply(mid);\n // Mark as delivered through the channel so the final outbound is skipped.\n editedAtLeastOnce = true;\n messageId = mid ?? messageId;\n // If we were in card mode, stop trying to update the card.\n cardId = undefined;\n } catch (err) {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming fallback send failed');\n }\n };\n\n const flush = async () => {\n if (aborted) return;\n if (ready) await ready;\n if (!messageId) return;\n const text = lastText;\n if (!text.trim()) return;\n try {\n await edit(text);\n } catch (err) {\n // Don't crash the agent loop. In card mode, card streaming can fail due to missing CardKit scopes;\n // fall back to sending a plain text reply so the user never gets stuck on \"Thinking…\".\n log.warn({ err, accountId: account.accountId, messageId, mode: preferCard ? 'card' : 'text' }, 'Feishu streaming edit failed');\n if (preferCard) {\n await sendFallbackText(text);\n }\n }\n };\n\n const start = async () => {\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n const thinking = 'Thinking…';\n\n if (preferCard) {\n const cardSpec = {\n schema: '2.0',\n config: { update_multi: true },\n header: {\n title: { tag: 'plain_text', content: 'xopc' },\n },\n body: {\n elements: [\n {\n tag: 'markdown',\n element_id: cardElementId,\n content: thinking,\n },\n ],\n },\n };\n\n const c = await (api as any).cardkit.v1.card.create({\n data: { type: 'card_json', data: JSON.stringify(cardSpec) },\n });\n cardId = c?.data?.card_id ?? c?.card_id ?? undefined;\n\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'card' },\n 'Feishu streaming started',\n );\n return;\n }\n\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'text' },\n 'Feishu streaming started',\n );\n };\n\n ready = start().catch((err) => {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming start failed');\n });\n\n return {\n update: (text: string) => {\n lastText = text;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n void flush().catch((err) => log.warn({ err }, 'Feishu streaming flush failed'));\n }, 800);\n },\n end: async () => {\n if (timer) clearTimeout(timer);\n await flush();\n log.debug(\n {\n accountId: account.accountId,\n chatId: options.chatId,\n messageId,\n editedAtLeastOnce,\n lastTextLen: lastText.length,\n mode: preferCard ? 'card' : 'text',\n },\n 'Feishu streaming ended',\n );\n },\n abort: async () => {\n aborted = true;\n if (timer) clearTimeout(timer);\n },\n messageId: () => undefined,\n skipFinalOutbound: () => Boolean(messageId) && editedAtLeastOnce,\n updateProgress: undefined,\n setProgress: undefined,\n };\n },\n };\n}\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"streaming-adapter.js","names":[],"sources":["../../../../../extensions/feishu/src/streaming/streaming-adapter.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type {\n ChannelStreamHandle,\n ChannelStreamingAdapter,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport { createLogger } from '@xopcai/xopc/utils/logger.js';\n\nimport { resolveFeishuAccount } from '../state/accounts.js';\nimport { formatFeishuOutboundText } from '../format.js';\nimport { getFeishuBindingByMessageId, recordFeishuMessageBinding } from '../state/message-bindings.js';\nimport { createFeishuClient } from '../transport/client/client.js';\n\nconst log = createLogger('FeishuStreaming');\n\n/** Normalize CardKit `card.create` responses (SDK / OpenAPI envelope shapes differ). */\nfunction extractFeishuCardKitCreateCardId(raw: unknown): string | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const o = raw as Record<string, unknown>;\n const tryId = (v: unknown): string | undefined => {\n if (typeof v !== 'string') return undefined;\n const t = v.trim();\n return t.length > 0 ? t : undefined;\n };\n\n const fromData = (data: unknown): string | undefined => {\n if (!data || typeof data !== 'object') return undefined;\n const d = data as Record<string, unknown>;\n return tryId(d.card_id) ?? tryId(d.cardId);\n };\n\n return (\n fromData(o.data) ??\n (o.data && typeof o.data === 'object' ? fromData((o.data as Record<string, unknown>).data) : undefined) ??\n tryId(o.card_id)\n );\n}\n\nfunction feishuOpenApiOk(res: unknown): boolean {\n if (res === null || res === undefined) return false;\n if (typeof res !== 'object') return true;\n const code = (res as Record<string, unknown>).code;\n if (code === undefined || code === null) return true;\n return Number(code) === 0;\n}\n\nexport function createFeishuStreamingAdapter(getConfig: () => Config): ChannelStreamingAdapter {\n return {\n startStream(options: {\n chatId: string;\n accountId?: string;\n threadId?: string;\n replyToMessageId?: string;\n parseMode?: 'Markdown' | 'HTML';\n }): ChannelStreamHandle | null {\n const cfg = getConfig();\n const account = resolveFeishuAccount(cfg, options.accountId ?? 'default');\n if (!account.configured) {\n return null;\n }\n // Feishu streaming is opt-in. When omitted/false, fall back to normal final outbound.\n if (account.streaming !== true) {\n return null;\n }\n const { api } = createFeishuClient(account);\n\n let messageId: string | undefined;\n let lastText = '';\n let timer: NodeJS.Timeout | null = null;\n let aborted = false;\n let editedAtLeastOnce = false;\n let ready: Promise<void> | null = null;\n let cardId: string | undefined;\n let cardSeq = 0;\n const cardElementId = `md_${randomUUID().replace(/-/g, '')}`;\n let fallbackSent = false;\n\n const renderMode = account.renderMode ?? 'auto';\n const preferCard = renderMode === 'card' || renderMode === 'auto';\n\n const formatStreamText = (text: string) => {\n if (!text.trim()) return text;\n if (text.trim() === 'Thinking…') return text;\n const forCardMarkdown = Boolean(preferCard && cardId);\n return formatFeishuOutboundText({\n text,\n renderMode,\n forCardMarkdown,\n });\n };\n\n const recordBindingIfReply = (childMessageId: string) => {\n if (!childMessageId) return;\n const parentId = options.replyToMessageId;\n if (!parentId) return;\n const parent = getFeishuBindingByMessageId(parentId);\n if (!parent) return;\n recordFeishuMessageBinding({ ...parent, messageId: childMessageId });\n };\n\n const edit = async (text: string) => {\n if (ready) await ready;\n if (!messageId) return;\n const outbound = formatStreamText(text);\n if (cardId && preferCard) {\n cardSeq += 1;\n await (api as any).cardkit.v1.cardElement.content({\n path: { card_id: cardId, element_id: cardElementId },\n data: {\n content: outbound,\n sequence: cardSeq,\n uuid: `${cardId}:${cardSeq}`,\n },\n });\n } else {\n await (api as any).im.v1.message.update({\n path: { message_id: messageId },\n data: { msg_type: 'text', content: JSON.stringify({ text: outbound }) },\n });\n }\n editedAtLeastOnce = true;\n };\n\n const sendFallbackText = async (text: string) => {\n if (fallbackSent) return;\n fallbackSent = true;\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n try {\n const outbound = formatStreamText(text);\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n },\n });\n const mid = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (mid) recordBindingIfReply(mid);\n // Mark as delivered through the channel so the final outbound is skipped.\n editedAtLeastOnce = true;\n messageId = mid ?? messageId;\n // If we were in card mode, stop trying to update the card.\n cardId = undefined;\n } catch (err) {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming fallback send failed');\n }\n };\n\n const flush = async () => {\n if (aborted) return;\n if (ready) await ready;\n if (!messageId) return;\n const text = lastText;\n if (!text.trim()) return;\n try {\n await edit(text);\n } catch (err) {\n // Don't crash the agent loop. CardKit updates can fail (scopes, element conflicts); plain\n // `im.message.update` can also fail for some message states — always fall back to a text\n // reply so the user is not stuck on \"Thinking…\".\n log.warn({ err, accountId: account.accountId, messageId, mode: preferCard ? 'card' : 'text' }, 'Feishu streaming edit failed');\n await sendFallbackText(text);\n }\n };\n\n const start = async () => {\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n const thinking = 'Thinking…';\n\n if (preferCard) {\n const cardSpec = {\n schema: '2.0',\n config: { update_multi: true },\n header: {\n title: { tag: 'plain_text', content: 'xopc' },\n },\n body: {\n elements: [\n {\n tag: 'markdown',\n element_id: cardElementId,\n content: thinking,\n },\n ],\n },\n };\n\n let created: unknown;\n try {\n created = await (api as any).cardkit.v1.card.create({\n data: { type: 'card_json', data: JSON.stringify(cardSpec) },\n });\n } catch (err) {\n log.warn({ err, accountId: account.accountId }, 'Feishu cardkit.card.create threw; falling back to text stream');\n created = null;\n }\n\n const apiOk = feishuOpenApiOk(created);\n cardId = apiOk ? extractFeishuCardKitCreateCardId(created) : undefined;\n\n if (!apiOk) {\n log.warn(\n {\n accountId: account.accountId,\n code: (created as Record<string, unknown> | null)?.code,\n msg: (created as Record<string, unknown> | null)?.msg,\n },\n 'Feishu cardkit.card.create returned non-zero code; falling back to text stream',\n );\n } else if (!cardId) {\n log.warn(\n { accountId: account.accountId, responsePreview: JSON.stringify(created).slice(0, 400) },\n 'Feishu cardkit.card.create returned no card_id; falling back to text stream',\n );\n }\n\n if (cardId) {\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'card' },\n 'Feishu streaming started',\n );\n return;\n }\n\n // Clear so downstream edits use im.message.update (text), not CardKit.\n cardId = undefined;\n }\n\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'text' },\n 'Feishu streaming started',\n );\n };\n\n ready = start().catch((err) => {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming start failed');\n });\n\n return {\n update: (text: string) => {\n lastText = text;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n void flush().catch((err) => log.warn({ err }, 'Feishu streaming flush failed'));\n }, 800);\n },\n end: async () => {\n if (timer) clearTimeout(timer);\n await flush();\n log.debug(\n {\n accountId: account.accountId,\n chatId: options.chatId,\n messageId,\n editedAtLeastOnce,\n lastTextLen: lastText.length,\n mode: preferCard ? 'card' : 'text',\n },\n 'Feishu streaming ended',\n );\n },\n abort: async () => {\n aborted = true;\n if (timer) clearTimeout(timer);\n },\n messageId: () => undefined,\n skipFinalOutbound: () => Boolean(messageId) && editedAtLeastOnce,\n updateProgress: undefined,\n setProgress: undefined,\n };\n },\n };\n}\n\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "telegram",
|
|
3
3
|
"name": "Telegram Channel",
|
|
4
4
|
"description": "Bundled Telegram Bot channel (private workspace sources; ships inside @xopcai/xopc dist/)",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.49",
|
|
6
6
|
"kind": "channel",
|
|
7
7
|
"main": "src/index.ts",
|
|
8
8
|
"channels": ["telegram"],
|
|
@@ -135,7 +135,7 @@ function resolveWeixinAccount(cfg, accountId) {
|
|
|
135
135
|
enabled: channelEnabled && accountCfg.enabled !== false,
|
|
136
136
|
configured: Boolean(token),
|
|
137
137
|
name: accountCfg.name?.trim() || void 0,
|
|
138
|
-
dmPolicy: accountCfg.dmPolicy ?? "
|
|
138
|
+
dmPolicy: accountCfg.dmPolicy ?? "open",
|
|
139
139
|
allowFrom: accountCfg.allowFrom ?? [],
|
|
140
140
|
streamMode: accountCfg.streamMode,
|
|
141
141
|
routeTag,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accounts.js","names":["fs"],"sources":["../../../../../extensions/weixin/src/auth/accounts.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport { clearContextTokensForAccount } from '../messaging/inbound.js';\nimport { resolveWeixinRootDir } from '../storage/state-dir.js';\nimport { logger } from '../util/logger.js';\n\nimport { normalizeWeixinAccountId } from './weixin-account-id.js';\n\nexport { normalizeWeixinAccountId };\n\nexport const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';\nexport const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';\n\nfunction resolveWeixinStateDir(): string {\n return resolveWeixinRootDir();\n}\n\nfunction resolveAccountIndexPath(): string {\n return path.join(resolveWeixinStateDir(), 'accounts.json');\n}\n\nexport function listIndexedWeixinAccountIds(): string[] {\n const filePath = resolveAccountIndexPath();\n try {\n if (!fs.existsSync(filePath)) return [];\n const raw = fs.readFileSync(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((id): id is string => typeof id === 'string' && id.trim() !== '');\n } catch {\n return [];\n }\n}\n\nexport function registerWeixinAccountId(accountId: string): void {\n const dir = resolveWeixinStateDir();\n fs.mkdirSync(dir, { recursive: true });\n\n const existing = listIndexedWeixinAccountIds();\n if (existing.includes(accountId)) return;\n\n const updated = [...existing, accountId];\n fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), 'utf-8');\n}\n\nexport function unregisterWeixinAccountId(accountId: string): void {\n const existing = listIndexedWeixinAccountIds();\n const updated = existing.filter((id) => id !== accountId);\n if (updated.length !== existing.length) {\n fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), 'utf-8');\n }\n}\n\nexport function clearStaleAccountsForUserId(\n currentAccountId: string,\n userId: string,\n onClearContextTokens?: (accountId: string) => void,\n): void {\n if (!userId) return;\n const allIds = listIndexedWeixinAccountIds();\n for (const id of allIds) {\n if (id === currentAccountId) continue;\n const data = loadWeixinAccount(id);\n if (data?.userId?.trim() === userId) {\n logger.info(`clearStaleAccountsForUserId: removing stale account=${id} (same userId=${userId})`);\n onClearContextTokens?.(id);\n clearWeixinAccount(id);\n unregisterWeixinAccountId(id);\n }\n }\n}\n\nexport type WeixinAccountData = {\n token?: string;\n savedAt?: string;\n baseUrl?: string;\n userId?: string;\n};\n\nfunction resolveAccountsDir(): string {\n return path.join(resolveWeixinStateDir(), 'accounts');\n}\n\nfunction resolveAccountPath(accountId: string): string {\n return path.join(resolveAccountsDir(), `${accountId}.json`);\n}\n\nfunction readAccountFile(filePath: string): WeixinAccountData | null {\n try {\n if (fs.existsSync(filePath)) {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as WeixinAccountData;\n }\n } catch {\n // ignore\n }\n return null;\n}\n\nexport function loadWeixinAccount(accountId: string): WeixinAccountData | null {\n return readAccountFile(resolveAccountPath(accountId));\n}\n\nexport function saveWeixinAccount(\n accountId: string,\n update: { token?: string; baseUrl?: string; userId?: string },\n): void {\n const dir = resolveAccountsDir();\n fs.mkdirSync(dir, { recursive: true });\n\n const existing = loadWeixinAccount(accountId) ?? {};\n\n const token = update.token?.trim() || existing.token;\n const baseUrl = update.baseUrl?.trim() || existing.baseUrl;\n const userId =\n update.userId !== undefined ? update.userId.trim() || undefined : existing.userId?.trim() || undefined;\n\n const data: WeixinAccountData = {\n ...(token ? { token, savedAt: new Date().toISOString() } : {}),\n ...(baseUrl ? { baseUrl } : {}),\n ...(userId ? { userId } : {}),\n };\n\n const filePath = resolveAccountPath(accountId);\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');\n try {\n fs.chmodSync(filePath, 0o600);\n } catch {\n // best-effort\n }\n}\n\nexport function clearWeixinAccount(accountId: string): void {\n const dir = resolveAccountsDir();\n const accountFiles = [\n `${accountId}.json`,\n `${accountId}.sync.json`,\n `${accountId}.context-tokens.json`,\n ];\n for (const file of accountFiles) {\n try {\n fs.unlinkSync(path.join(dir, file));\n } catch {\n // ignore\n }\n }\n try {\n fs.unlinkSync(resolveFrameworkAllowFromPath(accountId));\n } catch {\n // ignore\n }\n}\n\nexport type ResolvedWeixinAccount = {\n accountId: string;\n baseUrl: string;\n cdnBaseUrl: string;\n token?: string;\n enabled: boolean;\n configured: boolean;\n name?: string;\n dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';\n allowFrom: string[];\n streamMode?: 'off' | 'partial' | 'block';\n routeTag?: string;\n debug?: boolean;\n};\n\ntype WeixinAccountCfg = {\n name?: string;\n enabled?: boolean;\n cdnBaseUrl?: string;\n routeTag?: number | string;\n dmPolicy?: ResolvedWeixinAccount['dmPolicy'];\n allowFrom?: string[];\n streamMode?: ResolvedWeixinAccount['streamMode'];\n debug?: boolean;\n accounts?: Record<string, WeixinAccountCfg>;\n};\n\nfunction weixinSection(cfg: Config): (WeixinAccountCfg & { accounts?: Record<string, WeixinAccountCfg> }) | undefined {\n return cfg.channels?.weixin as\n | (WeixinAccountCfg & { accounts?: Record<string, WeixinAccountCfg> })\n | undefined;\n}\n\nexport function listWeixinAccountIds(cfg: Config): string[] {\n const section = weixinSection(cfg);\n const fromConfig = section?.accounts ? Object.keys(section.accounts) : [];\n // Only merge disk index when the channel is explicitly enabled. Otherwise removed / disabled\n // config still leaves token files + accounts.json on disk and monitors would keep running.\n if (!section || section.enabled !== true) {\n return fromConfig;\n }\n const indexed = listIndexedWeixinAccountIds();\n return [...new Set([...indexed, ...fromConfig])];\n}\n\nexport function resolveWeixinAccount(cfg: Config, accountId?: string | null): ResolvedWeixinAccount {\n const raw = accountId?.trim();\n if (!raw) {\n throw new Error('weixin: accountId is required');\n }\n const id = normalizeWeixinAccountId(raw);\n const section = weixinSection(cfg);\n const fromAccount = section?.accounts?.[id];\n const { accounts: _omit, ...sectionRest } = section ?? {};\n void _omit;\n const accountCfg: WeixinAccountCfg = {\n ...sectionRest,\n ...(fromAccount ?? {}),\n };\n\n const accountData = loadWeixinAccount(id);\n const token = accountData?.token?.trim() || undefined;\n const stateBaseUrl = accountData?.baseUrl?.trim() || '';\n\n const routeTagRaw = accountCfg.routeTag;\n const routeTag =\n typeof routeTagRaw === 'number'\n ? String(routeTagRaw)\n : typeof routeTagRaw === 'string' && routeTagRaw.trim()\n ? routeTagRaw.trim()\n : undefined;\n\n const channelEnabled = section?.enabled === true;\n\n return {\n accountId: id,\n baseUrl: stateBaseUrl || DEFAULT_BASE_URL,\n cdnBaseUrl: accountCfg.cdnBaseUrl?.trim() || CDN_BASE_URL,\n token,\n enabled: channelEnabled && accountCfg.enabled !== false,\n configured: Boolean(token),\n name: accountCfg.name?.trim() || undefined,\n dmPolicy: accountCfg.dmPolicy ?? 'pairing',\n allowFrom: accountCfg.allowFrom ?? [],\n streamMode: accountCfg.streamMode,\n routeTag,\n debug: accountCfg.debug ?? false,\n };\n}\n\nexport function resolveFrameworkAllowFromPath(accountId: string): string {\n const base = 'xopc-weixin'.replace(/[\\\\/:*?\"<>|]/g, '_');\n const safeAccount = accountId.trim().toLowerCase().replace(/[\\\\/:*?\"<>|]/g, '_').replace(/\\.\\./g, '_');\n const { join } = path;\n const credRoot = process.env.XOPC_CREDENTIALS_DIR?.trim()\n ? process.env.XOPC_CREDENTIALS_DIR!\n : join(resolveWeixinRootDir(), 'credentials');\n return join(credRoot, `${base}-${safeAccount}-allowFrom.json`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"accounts.js","names":["fs"],"sources":["../../../../../extensions/weixin/src/auth/accounts.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\n\nimport { clearContextTokensForAccount } from '../messaging/inbound.js';\nimport { resolveWeixinRootDir } from '../storage/state-dir.js';\nimport { logger } from '../util/logger.js';\n\nimport { normalizeWeixinAccountId } from './weixin-account-id.js';\n\nexport { normalizeWeixinAccountId };\n\nexport const DEFAULT_BASE_URL = 'https://ilinkai.weixin.qq.com';\nexport const CDN_BASE_URL = 'https://novac2c.cdn.weixin.qq.com/c2c';\n\nfunction resolveWeixinStateDir(): string {\n return resolveWeixinRootDir();\n}\n\nfunction resolveAccountIndexPath(): string {\n return path.join(resolveWeixinStateDir(), 'accounts.json');\n}\n\nexport function listIndexedWeixinAccountIds(): string[] {\n const filePath = resolveAccountIndexPath();\n try {\n if (!fs.existsSync(filePath)) return [];\n const raw = fs.readFileSync(filePath, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return [];\n return parsed.filter((id): id is string => typeof id === 'string' && id.trim() !== '');\n } catch {\n return [];\n }\n}\n\nexport function registerWeixinAccountId(accountId: string): void {\n const dir = resolveWeixinStateDir();\n fs.mkdirSync(dir, { recursive: true });\n\n const existing = listIndexedWeixinAccountIds();\n if (existing.includes(accountId)) return;\n\n const updated = [...existing, accountId];\n fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), 'utf-8');\n}\n\nexport function unregisterWeixinAccountId(accountId: string): void {\n const existing = listIndexedWeixinAccountIds();\n const updated = existing.filter((id) => id !== accountId);\n if (updated.length !== existing.length) {\n fs.writeFileSync(resolveAccountIndexPath(), JSON.stringify(updated, null, 2), 'utf-8');\n }\n}\n\nexport function clearStaleAccountsForUserId(\n currentAccountId: string,\n userId: string,\n onClearContextTokens?: (accountId: string) => void,\n): void {\n if (!userId) return;\n const allIds = listIndexedWeixinAccountIds();\n for (const id of allIds) {\n if (id === currentAccountId) continue;\n const data = loadWeixinAccount(id);\n if (data?.userId?.trim() === userId) {\n logger.info(`clearStaleAccountsForUserId: removing stale account=${id} (same userId=${userId})`);\n onClearContextTokens?.(id);\n clearWeixinAccount(id);\n unregisterWeixinAccountId(id);\n }\n }\n}\n\nexport type WeixinAccountData = {\n token?: string;\n savedAt?: string;\n baseUrl?: string;\n userId?: string;\n};\n\nfunction resolveAccountsDir(): string {\n return path.join(resolveWeixinStateDir(), 'accounts');\n}\n\nfunction resolveAccountPath(accountId: string): string {\n return path.join(resolveAccountsDir(), `${accountId}.json`);\n}\n\nfunction readAccountFile(filePath: string): WeixinAccountData | null {\n try {\n if (fs.existsSync(filePath)) {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as WeixinAccountData;\n }\n } catch {\n // ignore\n }\n return null;\n}\n\nexport function loadWeixinAccount(accountId: string): WeixinAccountData | null {\n return readAccountFile(resolveAccountPath(accountId));\n}\n\nexport function saveWeixinAccount(\n accountId: string,\n update: { token?: string; baseUrl?: string; userId?: string },\n): void {\n const dir = resolveAccountsDir();\n fs.mkdirSync(dir, { recursive: true });\n\n const existing = loadWeixinAccount(accountId) ?? {};\n\n const token = update.token?.trim() || existing.token;\n const baseUrl = update.baseUrl?.trim() || existing.baseUrl;\n const userId =\n update.userId !== undefined ? update.userId.trim() || undefined : existing.userId?.trim() || undefined;\n\n const data: WeixinAccountData = {\n ...(token ? { token, savedAt: new Date().toISOString() } : {}),\n ...(baseUrl ? { baseUrl } : {}),\n ...(userId ? { userId } : {}),\n };\n\n const filePath = resolveAccountPath(accountId);\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');\n try {\n fs.chmodSync(filePath, 0o600);\n } catch {\n // best-effort\n }\n}\n\nexport function clearWeixinAccount(accountId: string): void {\n const dir = resolveAccountsDir();\n const accountFiles = [\n `${accountId}.json`,\n `${accountId}.sync.json`,\n `${accountId}.context-tokens.json`,\n ];\n for (const file of accountFiles) {\n try {\n fs.unlinkSync(path.join(dir, file));\n } catch {\n // ignore\n }\n }\n try {\n fs.unlinkSync(resolveFrameworkAllowFromPath(accountId));\n } catch {\n // ignore\n }\n}\n\nexport type ResolvedWeixinAccount = {\n accountId: string;\n baseUrl: string;\n cdnBaseUrl: string;\n token?: string;\n enabled: boolean;\n configured: boolean;\n name?: string;\n dmPolicy: 'pairing' | 'allowlist' | 'open' | 'disabled';\n allowFrom: string[];\n streamMode?: 'off' | 'partial' | 'block';\n routeTag?: string;\n debug?: boolean;\n};\n\ntype WeixinAccountCfg = {\n name?: string;\n enabled?: boolean;\n cdnBaseUrl?: string;\n routeTag?: number | string;\n dmPolicy?: ResolvedWeixinAccount['dmPolicy'];\n allowFrom?: string[];\n streamMode?: ResolvedWeixinAccount['streamMode'];\n debug?: boolean;\n accounts?: Record<string, WeixinAccountCfg>;\n};\n\nfunction weixinSection(cfg: Config): (WeixinAccountCfg & { accounts?: Record<string, WeixinAccountCfg> }) | undefined {\n return cfg.channels?.weixin as\n | (WeixinAccountCfg & { accounts?: Record<string, WeixinAccountCfg> })\n | undefined;\n}\n\nexport function listWeixinAccountIds(cfg: Config): string[] {\n const section = weixinSection(cfg);\n const fromConfig = section?.accounts ? Object.keys(section.accounts) : [];\n // Only merge disk index when the channel is explicitly enabled. Otherwise removed / disabled\n // config still leaves token files + accounts.json on disk and monitors would keep running.\n if (!section || section.enabled !== true) {\n return fromConfig;\n }\n const indexed = listIndexedWeixinAccountIds();\n return [...new Set([...indexed, ...fromConfig])];\n}\n\nexport function resolveWeixinAccount(cfg: Config, accountId?: string | null): ResolvedWeixinAccount {\n const raw = accountId?.trim();\n if (!raw) {\n throw new Error('weixin: accountId is required');\n }\n const id = normalizeWeixinAccountId(raw);\n const section = weixinSection(cfg);\n const fromAccount = section?.accounts?.[id];\n const { accounts: _omit, ...sectionRest } = section ?? {};\n void _omit;\n const accountCfg: WeixinAccountCfg = {\n ...sectionRest,\n ...(fromAccount ?? {}),\n };\n\n const accountData = loadWeixinAccount(id);\n const token = accountData?.token?.trim() || undefined;\n const stateBaseUrl = accountData?.baseUrl?.trim() || '';\n\n const routeTagRaw = accountCfg.routeTag;\n const routeTag =\n typeof routeTagRaw === 'number'\n ? String(routeTagRaw)\n : typeof routeTagRaw === 'string' && routeTagRaw.trim()\n ? routeTagRaw.trim()\n : undefined;\n\n const channelEnabled = section?.enabled === true;\n\n return {\n accountId: id,\n baseUrl: stateBaseUrl || DEFAULT_BASE_URL,\n cdnBaseUrl: accountCfg.cdnBaseUrl?.trim() || CDN_BASE_URL,\n token,\n enabled: channelEnabled && accountCfg.enabled !== false,\n configured: Boolean(token),\n name: accountCfg.name?.trim() || undefined,\n dmPolicy: accountCfg.dmPolicy ?? 'open',\n allowFrom: accountCfg.allowFrom ?? [],\n streamMode: accountCfg.streamMode,\n routeTag,\n debug: accountCfg.debug ?? false,\n };\n}\n\nexport function resolveFrameworkAllowFromPath(accountId: string): string {\n const base = 'xopc-weixin'.replace(/[\\\\/:*?\"<>|]/g, '_');\n const safeAccount = accountId.trim().toLowerCase().replace(/[\\\\/:*?\"<>|]/g, '_').replace(/\\.\\./g, '_');\n const { join } = path;\n const credRoot = process.env.XOPC_CREDENTIALS_DIR?.trim()\n ? process.env.XOPC_CREDENTIALS_DIR!\n : join(resolveWeixinRootDir(), 'credentials');\n return join(credRoot, `${base}-${safeAccount}-allowFrom.json`);\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ var init_config_schema = __esmMin((() => {
|
|
|
13
13
|
"allowlist",
|
|
14
14
|
"open",
|
|
15
15
|
"disabled"
|
|
16
|
-
]).default("
|
|
16
|
+
]).default("open"),
|
|
17
17
|
allowFrom: z.array(z.string()).default([]),
|
|
18
18
|
streamMode: z.enum([
|
|
19
19
|
"off",
|
|
@@ -29,7 +29,7 @@ var init_config_schema = __esmMin((() => {
|
|
|
29
29
|
"allowlist",
|
|
30
30
|
"open",
|
|
31
31
|
"disabled"
|
|
32
|
-
]).default("
|
|
32
|
+
]).default("open"),
|
|
33
33
|
allowFrom: z.array(z.string()).default([]),
|
|
34
34
|
debug: z.boolean().default(false),
|
|
35
35
|
streamMode: z.enum([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-schema.js","names":[],"sources":["../../../../extensions/weixin/src/config-schema.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const WeixinAccountConfigSchema = z.object({\n name: z.string().optional(),\n enabled: z.boolean().optional(),\n cdnBaseUrl: z.string().optional(),\n routeTag: z.union([z.string(), z.number()]).optional(),\n dmPolicy: z.enum(['pairing', 'allowlist', 'open', 'disabled']).default('
|
|
1
|
+
{"version":3,"file":"config-schema.js","names":[],"sources":["../../../../extensions/weixin/src/config-schema.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const WeixinAccountConfigSchema = z.object({\n name: z.string().optional(),\n enabled: z.boolean().optional(),\n cdnBaseUrl: z.string().optional(),\n routeTag: z.union([z.string(), z.number()]).optional(),\n dmPolicy: z.enum(['pairing', 'allowlist', 'open', 'disabled']).default('open'),\n allowFrom: z.array(z.string()).default([]),\n streamMode: z.enum(['off', 'partial', 'block']).optional(),\n debug: z.boolean().optional(),\n});\n\nexport const WeixinConfigSchema = z.object({\n enabled: z.boolean().default(false),\n dmPolicy: z.enum(['pairing', 'allowlist', 'open', 'disabled']).default('open'),\n allowFrom: z.array(z.string()).default([]),\n debug: z.boolean().default(false),\n streamMode: z.enum(['off', 'partial', 'block']).optional(),\n historyLimit: z.number().default(50),\n textChunkLimit: z.number().default(4000),\n routeTag: z.union([z.string(), z.number()]).optional(),\n accounts: z.record(z.string(), WeixinAccountConfigSchema).optional(),\n});\n\nexport type WeixinConfig = z.infer<typeof WeixinConfigSchema>;\n"],"mappings":";;;;;AAEa,6BAA4B,EAAE,OAAO;EAChD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,SAAS,EAAE,SAAS,CAAC,UAAU;EAC/B,YAAY,EAAE,QAAQ,CAAC,UAAU;EACjC,UAAU,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU;EACtD,UAAU,EAAE,KAAK;GAAC;GAAW;GAAa;GAAQ;GAAW,CAAC,CAAC,QAAQ,OAAO;EAC9E,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;EAC1C,YAAY,EAAE,KAAK;GAAC;GAAO;GAAW;GAAQ,CAAC,CAAC,UAAU;EAC1D,OAAO,EAAE,SAAS,CAAC,UAAU;EAC9B,CAAC;AAEW,sBAAqB,EAAE,OAAO;EACzC,SAAS,EAAE,SAAS,CAAC,QAAQ,MAAM;EACnC,UAAU,EAAE,KAAK;GAAC;GAAW;GAAa;GAAQ;GAAW,CAAC,CAAC,QAAQ,OAAO;EAC9E,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;EAC1C,OAAO,EAAE,SAAS,CAAC,QAAQ,MAAM;EACjC,YAAY,EAAE,KAAK;GAAC;GAAO;GAAW;GAAQ,CAAC,CAAC,UAAU;EAC1D,cAAc,EAAE,QAAQ,CAAC,QAAQ,GAAG;EACpC,gBAAgB,EAAE,QAAQ,CAAC,QAAQ,IAAK;EACxC,UAAU,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU;EACtD,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,0BAA0B,CAAC,UAAU;EACrE,CAAC"}
|
|
@@ -3,7 +3,7 @@ const weixinConfigSurface = { buildConfigSurface(cfg) {
|
|
|
3
3
|
const weixin = cfg.channels?.weixin;
|
|
4
4
|
return {
|
|
5
5
|
enabled: weixin?.enabled ?? false,
|
|
6
|
-
dmPolicy: weixin?.dmPolicy || "
|
|
6
|
+
dmPolicy: weixin?.dmPolicy || "open",
|
|
7
7
|
allowFrom: weixin?.allowFrom || [],
|
|
8
8
|
debug: weixin?.debug ?? false,
|
|
9
9
|
streamMode: weixin?.streamMode ?? "partial",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-surface.js","names":[],"sources":["../../../../extensions/weixin/src/config-surface.ts"],"sourcesContent":["import type { Config } from '@xopcai/xopc/config/index.js';\nimport type { ChannelConfigSurfaceAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\n\nexport const weixinConfigSurface: ChannelConfigSurfaceAdapter = {\n buildConfigSurface(cfg: Config): Record<string, unknown> {\n const weixin = cfg.channels?.weixin as Record<string, unknown> | undefined;\n return {\n enabled: weixin?.enabled ?? false,\n dmPolicy: weixin?.dmPolicy || '
|
|
1
|
+
{"version":3,"file":"config-surface.js","names":[],"sources":["../../../../extensions/weixin/src/config-surface.ts"],"sourcesContent":["import type { Config } from '@xopcai/xopc/config/index.js';\nimport type { ChannelConfigSurfaceAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\n\nexport const weixinConfigSurface: ChannelConfigSurfaceAdapter = {\n buildConfigSurface(cfg: Config): Record<string, unknown> {\n const weixin = cfg.channels?.weixin as Record<string, unknown> | undefined;\n return {\n enabled: weixin?.enabled ?? false,\n dmPolicy: weixin?.dmPolicy || 'open',\n allowFrom: weixin?.allowFrom || [],\n debug: weixin?.debug ?? false,\n streamMode: weixin?.streamMode ?? 'partial',\n historyLimit: weixin?.historyLimit ?? 50,\n textChunkLimit: weixin?.textChunkLimit ?? 4000,\n routeTag: weixin?.routeTag,\n accounts: weixin?.accounts || {},\n };\n },\n};\n"],"mappings":";AAGA,MAAa,sBAAmD,EAC9D,mBAAmB,KAAsC;CACvD,MAAM,SAAS,IAAI,UAAU;AAC7B,QAAO;EACL,SAAS,QAAQ,WAAW;EAC5B,UAAU,QAAQ,YAAY;EAC9B,WAAW,QAAQ,aAAa,EAAE;EAClC,OAAO,QAAQ,SAAS;EACxB,YAAY,QAAQ,cAAc;EAClC,cAAc,QAAQ,gBAAgB;EACtC,gBAAgB,QAAQ,kBAAkB;EAC1C,UAAU,QAAQ;EAClB,UAAU,QAAQ,YAAY,EAAE;EACjC;GAEJ"}
|
|
@@ -61,7 +61,7 @@ async function processWeixinInboundMessage(full, deps) {
|
|
|
61
61
|
isGroup: false,
|
|
62
62
|
isDm: true
|
|
63
63
|
},
|
|
64
|
-
dmPolicy: deps.account.dmPolicy ?? "
|
|
64
|
+
dmPolicy: deps.account.dmPolicy ?? "open",
|
|
65
65
|
allowFrom: mergedAllow
|
|
66
66
|
});
|
|
67
67
|
if (!access.allowed) {
|
|
@@ -91,7 +91,7 @@ async function processWeixinInboundMessage(full, deps) {
|
|
|
91
91
|
} catch (err) {
|
|
92
92
|
logger.warn(`weixin pairing prompt failed from=${senderId} err=${String(err)}`);
|
|
93
93
|
}
|
|
94
|
-
else logger.info(`weixin: dropped message from=${senderId} dmPolicy=${deps.account.dmPolicy ?? "
|
|
94
|
+
else logger.info(`weixin: dropped message from=${senderId} dmPolicy=${deps.account.dmPolicy ?? "open"} reason=${access.reason ?? "not allowed"}`);
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const mediaOpts = {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-message.js","names":[],"sources":["../../../../../extensions/weixin/src/messaging/process-message.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { randomBytes } from 'node:crypto';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { MessageBus } from '@xopcai/xopc/infra/bus/index.js';\nimport { generateSessionKey } from '@xopcai/xopc/chat-commands/session-key.js';\nimport { issuePairingChallenge, resolveWeixinPairingPath } from '@xopcai/xopc/channels/pairing/index.js';\nimport { evaluateAccess } from '@xopcai/xopc/channels/security.js';\nimport type { WeixinMessage } from '../api/types.js';\nimport { MessageItemType } from '../api/types.js';\nimport type { ResolvedWeixinAccount } from '../auth/accounts.js';\nimport { readFrameworkAllowFromList } from '../auth/pairing.js';\nimport { downloadMediaFromItem } from '../media/media-download.js';\nimport { logger } from '../util/logger.js';\nimport { resolveWeixinRootDir } from '../storage/state-dir.js';\nimport { handleSlashCommand } from './slash-commands.js';\nimport { sendMessageWeixin } from './send.js';\nimport {\n setContextToken,\n weixinMessageToMsgContext,\n isMediaItem,\n type WeixinInboundMediaOpts,\n} from './inbound.js';\nimport { isDebugMode } from './debug-mode.js';\n\nfunction extractTextBody(itemList?: import('../api/types.js').MessageItem[]): string {\n if (!itemList?.length) return '';\n for (const item of itemList) {\n if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {\n return String(item.text_item.text);\n }\n }\n return '';\n}\n\nfunction mergeAllowFrom(account: ResolvedWeixinAccount, accountId: string): string[] {\n const store = readFrameworkAllowFromList(accountId);\n return [...new Set([...(account.allowFrom ?? []).map(String), ...store])];\n}\n\nasync function saveMediaBuffer(\n buffer: Buffer,\n contentType?: string,\n _subdir?: string,\n maxBytes?: number,\n originalFilename?: string,\n): Promise<{ path: string }> {\n const cap = maxBytes ?? 100 * 1024 * 1024;\n if (buffer.length > cap) {\n throw new Error(`weixin media exceeds max bytes (${cap})`);\n }\n const dir = path.join(resolveWeixinRootDir(), 'media', 'inbound-temp');\n await mkdir(dir, { recursive: true });\n const ext = originalFilename ? path.extname(originalFilename) : contentType?.includes('wav') ? '.wav' : '.bin';\n const fname = `${Date.now()}-${randomBytes(6).toString('hex')}${ext || '.bin'}`;\n const filePath = path.join(dir, fname);\n await writeFile(filePath, buffer);\n return { path: filePath };\n}\n\nexport type ProcessWeixinMessageDeps = {\n accountId: string;\n account: ResolvedWeixinAccount;\n config: Config;\n bus: MessageBus;\n baseUrl: string;\n cdnBaseUrl: string;\n token?: string;\n routeTag?: string;\n};\n\nexport async function processWeixinInboundMessage(\n full: WeixinMessage,\n deps: ProcessWeixinMessageDeps,\n): Promise<void> {\n const receivedAt = Date.now();\n const senderId = full.from_user_id ?? '';\n // Persist ilink context_token before slash handling and DM policy. Pairing only gates the agent\n // pipeline; cron/tools still need a cached token to call sendmessage.\n if (full.context_token?.trim() && senderId) {\n setContextToken(deps.accountId, senderId, full.context_token, { sendToUserId: senderId });\n }\n\n const textBody = extractTextBody(full.item_list);\n\n if (textBody.startsWith('/')) {\n const slashResult = await handleSlashCommand(\n textBody,\n {\n to: full.from_user_id ?? '',\n contextToken: full.context_token,\n baseUrl: deps.baseUrl,\n token: deps.token,\n accountId: deps.accountId,\n log: () => {},\n errLog: () => {},\n },\n receivedAt,\n full.create_time_ms,\n );\n if (slashResult.handled) {\n return;\n }\n }\n\n const mergedAllow = mergeAllowFrom(deps.account, deps.accountId);\n const access = evaluateAccess({\n context: {\n channel: 'weixin',\n accountId: deps.accountId,\n chatId: senderId,\n senderId,\n isGroup: false,\n isDm: true,\n },\n dmPolicy: deps.account.dmPolicy ?? 'pairing',\n allowFrom: mergedAllow,\n });\n if (!access.allowed) {\n if (access.reason === 'pairing-required' && deps.token) {\n try {\n await issuePairingChallenge({\n channel: 'weixin',\n pairingFilePath: resolveWeixinPairingPath(deps.accountId),\n accountId: deps.accountId,\n senderId,\n senderIdLine: `Your Weixin user id: ${senderId}`,\n sendPairingReply: async (text) => {\n await sendMessageWeixin({\n to: senderId,\n text,\n opts: {\n baseUrl: deps.baseUrl,\n token: deps.token,\n contextToken: full.context_token,\n routeTag: deps.routeTag,\n },\n });\n },\n onReplyError: (err) => {\n logger.warn(`weixin pairing reply failed from=${senderId} err=${String(err)}`);\n },\n });\n } catch (err) {\n logger.warn(`weixin pairing prompt failed from=${senderId} err=${String(err)}`);\n }\n } else {\n logger.info(\n `weixin: dropped message from=${senderId} dmPolicy=${deps.account.dmPolicy ?? 'pairing'} reason=${access.reason ?? 'not allowed'}`,\n );\n }\n return;\n }\n\n const mediaOpts: WeixinInboundMediaOpts = {};\n\n const hasDownloadableMedia = (m?: { encrypt_query_param?: string; full_url?: string }) =>\n Boolean(m?.encrypt_query_param || m?.full_url);\n\n const mainMediaItem =\n full.item_list?.find(\n (i) => i.type === MessageItemType.IMAGE && hasDownloadableMedia(i.image_item?.media),\n ) ??\n full.item_list?.find(\n (i) => i.type === MessageItemType.VIDEO && hasDownloadableMedia(i.video_item?.media),\n ) ??\n full.item_list?.find(\n (i) => i.type === MessageItemType.FILE && hasDownloadableMedia(i.file_item?.media),\n ) ??\n full.item_list?.find(\n (i) =>\n i.type === MessageItemType.VOICE &&\n hasDownloadableMedia(i.voice_item?.media) &&\n !i.voice_item?.text,\n );\n const refMediaItem = !mainMediaItem\n ? full.item_list?.find(\n (i) =>\n i.type === MessageItemType.TEXT &&\n i.ref_msg?.message_item &&\n isMediaItem(i.ref_msg.message_item!),\n )?.ref_msg?.message_item\n : undefined;\n\n const mediaItem = mainMediaItem ?? refMediaItem;\n if (mediaItem) {\n const label = refMediaItem ? 'ref' : 'inbound';\n const downloaded = await downloadMediaFromItem(mediaItem, {\n cdnBaseUrl: deps.cdnBaseUrl,\n saveMedia: saveMediaBuffer,\n log: (m) => logger.debug(m),\n errLog: (m) => logger.warn(m),\n label,\n });\n Object.assign(mediaOpts, downloaded);\n }\n\n const ctx = weixinMessageToMsgContext(full, deps.accountId, mediaOpts);\n const body = ctx.Body?.trim() ?? '';\n\n const sessionKey = generateSessionKey({\n source: 'weixin',\n chatId: senderId,\n senderId,\n isGroup: false,\n accountId: deps.accountId,\n });\n\n const attachments: Array<{ type: string; mimeType?: string; data?: string; name?: string }> = [];\n\n if (mediaOpts.decryptedPicPath) {\n const buf = await readFile(mediaOpts.decryptedPicPath);\n attachments.push({\n type: 'image',\n mimeType: 'image/jpeg',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedPicPath),\n });\n } else if (mediaOpts.decryptedVoicePath) {\n const buf = await readFile(mediaOpts.decryptedVoicePath);\n attachments.push({\n type: 'audio',\n mimeType: mediaOpts.voiceMediaType ?? 'audio/wav',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedVoicePath),\n });\n } else if (mediaOpts.decryptedFilePath) {\n const buf = await readFile(mediaOpts.decryptedFilePath);\n attachments.push({\n type: 'file',\n mimeType: mediaOpts.fileMediaType ?? 'application/octet-stream',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedFilePath),\n });\n } else if (mediaOpts.decryptedVideoPath) {\n const buf = await readFile(mediaOpts.decryptedVideoPath);\n attachments.push({\n type: 'file',\n mimeType: 'video/mp4',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedVideoPath),\n });\n }\n\n const debug = isDebugMode(deps.accountId);\n if (debug) {\n logger.debug(\n `weixin debug inbound from=${senderId} bodyLen=${body.length} attachments=${attachments.length}`,\n );\n }\n\n await deps.bus.publishInbound({\n channel: 'weixin',\n sender_id: senderId,\n chat_id: senderId,\n content: body,\n metadata: {\n accountId: deps.accountId,\n sessionKey,\n messageId: full.message_id != null ? String(full.message_id) : undefined,\n isGroup: false,\n isCommand: body.trim().startsWith('/'),\n contextToken: ctx.context_token,\n },\n attachments: attachments.length ? attachments : undefined,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"process-message.js","names":[],"sources":["../../../../../extensions/weixin/src/messaging/process-message.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { randomBytes } from 'node:crypto';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { MessageBus } from '@xopcai/xopc/infra/bus/index.js';\nimport { generateSessionKey } from '@xopcai/xopc/chat-commands/session-key.js';\nimport { issuePairingChallenge, resolveWeixinPairingPath } from '@xopcai/xopc/channels/pairing/index.js';\nimport { evaluateAccess } from '@xopcai/xopc/channels/security.js';\nimport type { WeixinMessage } from '../api/types.js';\nimport { MessageItemType } from '../api/types.js';\nimport type { ResolvedWeixinAccount } from '../auth/accounts.js';\nimport { readFrameworkAllowFromList } from '../auth/pairing.js';\nimport { downloadMediaFromItem } from '../media/media-download.js';\nimport { logger } from '../util/logger.js';\nimport { resolveWeixinRootDir } from '../storage/state-dir.js';\nimport { handleSlashCommand } from './slash-commands.js';\nimport { sendMessageWeixin } from './send.js';\nimport {\n setContextToken,\n weixinMessageToMsgContext,\n isMediaItem,\n type WeixinInboundMediaOpts,\n} from './inbound.js';\nimport { isDebugMode } from './debug-mode.js';\n\nfunction extractTextBody(itemList?: import('../api/types.js').MessageItem[]): string {\n if (!itemList?.length) return '';\n for (const item of itemList) {\n if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {\n return String(item.text_item.text);\n }\n }\n return '';\n}\n\nfunction mergeAllowFrom(account: ResolvedWeixinAccount, accountId: string): string[] {\n const store = readFrameworkAllowFromList(accountId);\n return [...new Set([...(account.allowFrom ?? []).map(String), ...store])];\n}\n\nasync function saveMediaBuffer(\n buffer: Buffer,\n contentType?: string,\n _subdir?: string,\n maxBytes?: number,\n originalFilename?: string,\n): Promise<{ path: string }> {\n const cap = maxBytes ?? 100 * 1024 * 1024;\n if (buffer.length > cap) {\n throw new Error(`weixin media exceeds max bytes (${cap})`);\n }\n const dir = path.join(resolveWeixinRootDir(), 'media', 'inbound-temp');\n await mkdir(dir, { recursive: true });\n const ext = originalFilename ? path.extname(originalFilename) : contentType?.includes('wav') ? '.wav' : '.bin';\n const fname = `${Date.now()}-${randomBytes(6).toString('hex')}${ext || '.bin'}`;\n const filePath = path.join(dir, fname);\n await writeFile(filePath, buffer);\n return { path: filePath };\n}\n\nexport type ProcessWeixinMessageDeps = {\n accountId: string;\n account: ResolvedWeixinAccount;\n config: Config;\n bus: MessageBus;\n baseUrl: string;\n cdnBaseUrl: string;\n token?: string;\n routeTag?: string;\n};\n\nexport async function processWeixinInboundMessage(\n full: WeixinMessage,\n deps: ProcessWeixinMessageDeps,\n): Promise<void> {\n const receivedAt = Date.now();\n const senderId = full.from_user_id ?? '';\n // Persist ilink context_token before slash handling and DM policy. Pairing only gates the agent\n // pipeline; cron/tools still need a cached token to call sendmessage.\n if (full.context_token?.trim() && senderId) {\n setContextToken(deps.accountId, senderId, full.context_token, { sendToUserId: senderId });\n }\n\n const textBody = extractTextBody(full.item_list);\n\n if (textBody.startsWith('/')) {\n const slashResult = await handleSlashCommand(\n textBody,\n {\n to: full.from_user_id ?? '',\n contextToken: full.context_token,\n baseUrl: deps.baseUrl,\n token: deps.token,\n accountId: deps.accountId,\n log: () => {},\n errLog: () => {},\n },\n receivedAt,\n full.create_time_ms,\n );\n if (slashResult.handled) {\n return;\n }\n }\n\n const mergedAllow = mergeAllowFrom(deps.account, deps.accountId);\n const access = evaluateAccess({\n context: {\n channel: 'weixin',\n accountId: deps.accountId,\n chatId: senderId,\n senderId,\n isGroup: false,\n isDm: true,\n },\n dmPolicy: deps.account.dmPolicy ?? 'open',\n allowFrom: mergedAllow,\n });\n if (!access.allowed) {\n if (access.reason === 'pairing-required' && deps.token) {\n try {\n await issuePairingChallenge({\n channel: 'weixin',\n pairingFilePath: resolveWeixinPairingPath(deps.accountId),\n accountId: deps.accountId,\n senderId,\n senderIdLine: `Your Weixin user id: ${senderId}`,\n sendPairingReply: async (text) => {\n await sendMessageWeixin({\n to: senderId,\n text,\n opts: {\n baseUrl: deps.baseUrl,\n token: deps.token,\n contextToken: full.context_token,\n routeTag: deps.routeTag,\n },\n });\n },\n onReplyError: (err) => {\n logger.warn(`weixin pairing reply failed from=${senderId} err=${String(err)}`);\n },\n });\n } catch (err) {\n logger.warn(`weixin pairing prompt failed from=${senderId} err=${String(err)}`);\n }\n } else {\n logger.info(\n `weixin: dropped message from=${senderId} dmPolicy=${deps.account.dmPolicy ?? 'open'} reason=${access.reason ?? 'not allowed'}`,\n );\n }\n return;\n }\n\n const mediaOpts: WeixinInboundMediaOpts = {};\n\n const hasDownloadableMedia = (m?: { encrypt_query_param?: string; full_url?: string }) =>\n Boolean(m?.encrypt_query_param || m?.full_url);\n\n const mainMediaItem =\n full.item_list?.find(\n (i) => i.type === MessageItemType.IMAGE && hasDownloadableMedia(i.image_item?.media),\n ) ??\n full.item_list?.find(\n (i) => i.type === MessageItemType.VIDEO && hasDownloadableMedia(i.video_item?.media),\n ) ??\n full.item_list?.find(\n (i) => i.type === MessageItemType.FILE && hasDownloadableMedia(i.file_item?.media),\n ) ??\n full.item_list?.find(\n (i) =>\n i.type === MessageItemType.VOICE &&\n hasDownloadableMedia(i.voice_item?.media) &&\n !i.voice_item?.text,\n );\n const refMediaItem = !mainMediaItem\n ? full.item_list?.find(\n (i) =>\n i.type === MessageItemType.TEXT &&\n i.ref_msg?.message_item &&\n isMediaItem(i.ref_msg.message_item!),\n )?.ref_msg?.message_item\n : undefined;\n\n const mediaItem = mainMediaItem ?? refMediaItem;\n if (mediaItem) {\n const label = refMediaItem ? 'ref' : 'inbound';\n const downloaded = await downloadMediaFromItem(mediaItem, {\n cdnBaseUrl: deps.cdnBaseUrl,\n saveMedia: saveMediaBuffer,\n log: (m) => logger.debug(m),\n errLog: (m) => logger.warn(m),\n label,\n });\n Object.assign(mediaOpts, downloaded);\n }\n\n const ctx = weixinMessageToMsgContext(full, deps.accountId, mediaOpts);\n const body = ctx.Body?.trim() ?? '';\n\n const sessionKey = generateSessionKey({\n source: 'weixin',\n chatId: senderId,\n senderId,\n isGroup: false,\n accountId: deps.accountId,\n });\n\n const attachments: Array<{ type: string; mimeType?: string; data?: string; name?: string }> = [];\n\n if (mediaOpts.decryptedPicPath) {\n const buf = await readFile(mediaOpts.decryptedPicPath);\n attachments.push({\n type: 'image',\n mimeType: 'image/jpeg',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedPicPath),\n });\n } else if (mediaOpts.decryptedVoicePath) {\n const buf = await readFile(mediaOpts.decryptedVoicePath);\n attachments.push({\n type: 'audio',\n mimeType: mediaOpts.voiceMediaType ?? 'audio/wav',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedVoicePath),\n });\n } else if (mediaOpts.decryptedFilePath) {\n const buf = await readFile(mediaOpts.decryptedFilePath);\n attachments.push({\n type: 'file',\n mimeType: mediaOpts.fileMediaType ?? 'application/octet-stream',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedFilePath),\n });\n } else if (mediaOpts.decryptedVideoPath) {\n const buf = await readFile(mediaOpts.decryptedVideoPath);\n attachments.push({\n type: 'file',\n mimeType: 'video/mp4',\n data: buf.toString('base64'),\n name: path.basename(mediaOpts.decryptedVideoPath),\n });\n }\n\n const debug = isDebugMode(deps.accountId);\n if (debug) {\n logger.debug(\n `weixin debug inbound from=${senderId} bodyLen=${body.length} attachments=${attachments.length}`,\n );\n }\n\n await deps.bus.publishInbound({\n channel: 'weixin',\n sender_id: senderId,\n chat_id: senderId,\n content: body,\n metadata: {\n accountId: deps.accountId,\n sessionKey,\n messageId: full.message_id != null ? String(full.message_id) : undefined,\n isGroup: false,\n isCommand: body.trim().startsWith('/'),\n contextToken: ctx.context_token,\n },\n attachments: attachments.length ? attachments : undefined,\n });\n}\n"],"mappings":"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"}
|
|
@@ -93,7 +93,7 @@ var WeixinChannelPlugin = class {
|
|
|
93
93
|
})
|
|
94
94
|
};
|
|
95
95
|
security = {
|
|
96
|
-
resolveDmPolicy: ({ account }) => resolveDmPolicy(account.dmPolicy, "
|
|
96
|
+
resolveDmPolicy: ({ account }) => resolveDmPolicy(account.dmPolicy, "open"),
|
|
97
97
|
checkAccess: (ctx, account, _cfg) => {
|
|
98
98
|
const allowFrom = [...account.allowFrom ?? [], ...readFrameworkAllowFromList(account.accountId)];
|
|
99
99
|
return evaluateAccess({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":[],"sources":["../../../../extensions/weixin/src/plugin.ts"],"sourcesContent":["/**\n * Weixin (WeChat ilink) channel — long-poll getUpdates, QR login, direct messages only.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { MessageBus } from '@xopcai/xopc/infra/bus/index.js';\nimport type {\n ChannelCapabilities,\n ChannelPlugin,\n ChannelPluginDefaults,\n ChannelPluginInitOptions,\n ChannelPluginReloadMeta,\n ChannelPluginStartOptions,\n ChannelSecurityContext,\n ChannelStreamingAdapter,\n ChatType,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport { evaluateAccess, resolveDmPolicy } from '@xopcai/xopc/channels/security.js';\nimport { createLogger } from '@xopcai/xopc/utils/logger.js';\n\nimport { restoreContextTokens } from './messaging/inbound.js';\nimport { monitorWeixinProvider } from './monitor/monitor.js';\nimport type { ChannelCliLoginAdapter, ChannelCronDeliveryAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\nimport {\n listWeixinAccountIds,\n resolveWeixinAccount,\n type ResolvedWeixinAccount,\n} from './auth/accounts.js';\nimport { readFrameworkAllowFromList } from './auth/pairing.js';\nimport { createWeixinOutboundHandlers, weixinTextChunker } from './outbound-send.js';\nimport { normalizeWeixinCronDeliveryToResolved } from './delivery-to.js';\nimport { weixinConfigSurface } from './config-surface.js';\nimport { WeixinConfigSchema } from './config-schema.js';\nimport { weixinOnboardAdapter } from './adapters/onboard-cli.js';\n\nconst log = createLogger('WeixinPlugin');\n\nexport class WeixinChannelPlugin implements ChannelPlugin<ResolvedWeixinAccount> {\n readonly id = 'weixin' as const;\n\n readonly reload: ChannelPluginReloadMeta = {\n configPrefixes: ['channels.weixin'],\n };\n\n readonly meta = {\n id: 'weixin',\n label: 'Weixin',\n selectionLabel: 'Weixin (ilink)',\n docsPath: '/channels/weixin',\n blurb: 'WeChat via Tencent ilink bot API (QR login, direct chat).',\n order: 3,\n deferConnectUntilAfterListen: true,\n } as const;\n\n readonly capabilities: ChannelCapabilities = {\n chatTypes: ['direct'] as ChatType[],\n reactions: false,\n threads: false,\n media: true,\n polls: false,\n nativeCommands: false,\n blockStreaming: true,\n };\n\n readonly configSchema = {\n schema: {},\n validate: (raw: unknown) => {\n const r = WeixinConfigSchema.safeParse(raw);\n return r.success ? { ok: true as const } : { ok: false as const, errors: [r.error.message] };\n },\n };\n\n readonly cronDelivery: ChannelCronDeliveryAdapter = {\n async normalizeDeliveryTarget(to, sessionStore) {\n const { chatId, accountId } = await normalizeWeixinCronDeliveryToResolved(to, sessionStore);\n return { chatId, accountId };\n },\n };\n\n readonly onboard = weixinOnboardAdapter;\n\n readonly cliLogin: ChannelCliLoginAdapter = {\n async runLogin(params) {\n const { runWeixinQrLoginCli } = await import('./cli/qr-login.js');\n return runWeixinQrLoginCli({\n configPath: params.configPath,\n verbose: params.verbose,\n timeoutMs: params.timeoutMs,\n account: params.accountId,\n writeConfig: params.writeConfig,\n });\n },\n };\n\n readonly configSurface = weixinConfigSurface;\n\n readonly defaults: ChannelPluginDefaults = {\n queue: { debounceMs: 0 },\n outbound: { textChunkLimit: 4000 },\n streaming: {\n blockStreamingCoalesce: {\n minChars: 200,\n idleMs: 3000,\n },\n },\n };\n\n private bus!: ChannelPluginInitOptions['bus'];\n private cfg!: Config;\n private abortControllers = new Map<string, AbortController>();\n\n config = {\n listAccountIds: (cfg: Config) => listWeixinAccountIds(cfg),\n resolveAccount: (cfg: Config, accountId?: string | null) => resolveWeixinAccount(cfg, accountId),\n isConfigured: async (account: ResolvedWeixinAccount) => account.configured,\n describeAccount: (account: ResolvedWeixinAccount, _cfg: Config) => ({\n accountId: account.accountId,\n channelId: 'weixin',\n name: account.name,\n enabled: account.enabled,\n configured: account.configured,\n status: undefined,\n }),\n };\n\n security = {\n resolveDmPolicy: ({ account }: { account: ResolvedWeixinAccount }) =>\n resolveDmPolicy(account.dmPolicy, 'pairing'),\n checkAccess: (ctx: ChannelSecurityContext, account: ResolvedWeixinAccount, _cfg: Config) => {\n const allowFrom = [...(account.allowFrom ?? []), ...readFrameworkAllowFromList(account.accountId)];\n return evaluateAccess({\n context: {\n channel: 'weixin',\n accountId: account.accountId,\n chatId: ctx.chatId,\n senderId: ctx.senderId,\n senderName: ctx.senderName,\n isGroup: false,\n isDm: true,\n },\n dmPolicy: account.dmPolicy,\n allowFrom,\n });\n },\n };\n\n outbound = {\n deliveryMode: 'direct' as const,\n chunker: weixinTextChunker,\n chunkerMode: 'text' as const,\n textChunkLimit: 4000,\n ...createWeixinOutboundHandlers(),\n };\n\n streaming: ChannelStreamingAdapter = {\n startStream: () => null,\n };\n\n /** Channel plugin hints (cron targets, media paths). */\n agentPrompt = {\n augmentSystemPrompt: (): string =>\n [\n 'Weixin (ilink): direct chat only. To send an image or file, use the message tool with action send and set media to a local absolute path or an HTTPS URL; relative paths may fail.',\n 'For cron or scheduled delivery to a Weixin contact, set delivery.to to the user Weixin id (ending in @im.wechat) and delivery.accountId to the bot account id, or outbound may pick the wrong account.',\n 'When using MEDIA: to attach a file, put the MEDIA: line alone on its own line, not inline with other text.',\n ].join('\\n'),\n };\n\n async init(options: ChannelPluginInitOptions): Promise<void> {\n this.bus = options.bus;\n this.cfg = options.config;\n log.debug('Weixin plugin initialized');\n }\n\n async start(options?: ChannelPluginStartOptions): Promise<void> {\n const ids = options?.accountId\n ? [options.accountId]\n : listWeixinAccountIds(this.cfg);\n\n for (const accountId of ids) {\n const account = resolveWeixinAccount(this.cfg, accountId);\n if (!account.enabled || !account.configured || !account.token) continue;\n\n if (this.abortControllers.has(accountId)) continue;\n\n restoreContextTokens(account.accountId);\n\n const ac = new AbortController();\n this.abortControllers.set(accountId, ac);\n\n void monitorWeixinProvider({\n account,\n config: this.cfg,\n bus: this.bus,\n abortSignal: ac.signal,\n }).catch((err) => {\n if ((err as { name?: string; message?: string } | undefined)?.name === 'AbortError') {\n log.debug({ accountId }, 'Weixin monitor stopped');\n return;\n }\n log.error({ err, accountId }, 'Weixin monitor exited with error');\n });\n\n log.info({ accountId }, 'Weixin monitor started');\n }\n }\n\n async stop(accountId?: string): Promise<void> {\n const ids = accountId ? [accountId] : [...this.abortControllers.keys()];\n for (const id of ids) {\n const ac = this.abortControllers.get(id);\n if (ac) {\n ac.abort();\n this.abortControllers.delete(id);\n }\n }\n }\n\n channelIsRunning(cfg: Config): boolean {\n return listWeixinAccountIds(cfg).some((id) => {\n const a = resolveWeixinAccount(cfg, id);\n return a.configured && a.enabled !== false && this.abortControllers.has(id);\n });\n }\n\n async onConfigUpdated(cfg: Config): Promise<void> {\n const prevWx = this.cfg.channels?.weixin as unknown;\n const nextWx = cfg.channels?.weixin as { enabled?: boolean } | undefined;\n const channelOff = !nextWx || nextWx.enabled !== true;\n\n if (channelOff) {\n this.cfg = cfg;\n await this.stop();\n return;\n }\n\n this.cfg = cfg;\n\n if (isDeepStrictEqual(prevWx, nextWx) && this.channelIsRunning(cfg)) {\n return;\n }\n\n await this.stop();\n await this.start();\n }\n\n /**\n * Restart long-poll monitors after credentials were written to disk without a `channels.weixin` JSON\n * delta (e.g. only token files / account index updated). Gateway calls this after QR login completes.\n *\n * Pass `bus` explicitly: if Weixin was disabled at gateway boot, `init()` was skipped and `this.bus` was never set.\n */\n async reloadMonitorsWithConfig(cfg: Config, bus: MessageBus): Promise<void> {\n this.bus = bus;\n this.cfg = cfg;\n await this.stop();\n await this.start();\n }\n}\n\nexport const weixinPlugin = new WeixinChannelPlugin();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":[],"sources":["../../../../extensions/weixin/src/plugin.ts"],"sourcesContent":["/**\n * Weixin (WeChat ilink) channel — long-poll getUpdates, QR login, direct messages only.\n */\n\nimport { isDeepStrictEqual } from 'node:util';\n\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport type { MessageBus } from '@xopcai/xopc/infra/bus/index.js';\nimport type {\n ChannelCapabilities,\n ChannelPlugin,\n ChannelPluginDefaults,\n ChannelPluginInitOptions,\n ChannelPluginReloadMeta,\n ChannelPluginStartOptions,\n ChannelSecurityContext,\n ChannelStreamingAdapter,\n ChatType,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport { evaluateAccess, resolveDmPolicy } from '@xopcai/xopc/channels/security.js';\nimport { createLogger } from '@xopcai/xopc/utils/logger.js';\n\nimport { restoreContextTokens } from './messaging/inbound.js';\nimport { monitorWeixinProvider } from './monitor/monitor.js';\nimport type { ChannelCliLoginAdapter, ChannelCronDeliveryAdapter } from '@xopcai/xopc/channels/plugins/types.adapters.js';\nimport {\n listWeixinAccountIds,\n resolveWeixinAccount,\n type ResolvedWeixinAccount,\n} from './auth/accounts.js';\nimport { readFrameworkAllowFromList } from './auth/pairing.js';\nimport { createWeixinOutboundHandlers, weixinTextChunker } from './outbound-send.js';\nimport { normalizeWeixinCronDeliveryToResolved } from './delivery-to.js';\nimport { weixinConfigSurface } from './config-surface.js';\nimport { WeixinConfigSchema } from './config-schema.js';\nimport { weixinOnboardAdapter } from './adapters/onboard-cli.js';\n\nconst log = createLogger('WeixinPlugin');\n\nexport class WeixinChannelPlugin implements ChannelPlugin<ResolvedWeixinAccount> {\n readonly id = 'weixin' as const;\n\n readonly reload: ChannelPluginReloadMeta = {\n configPrefixes: ['channels.weixin'],\n };\n\n readonly meta = {\n id: 'weixin',\n label: 'Weixin',\n selectionLabel: 'Weixin (ilink)',\n docsPath: '/channels/weixin',\n blurb: 'WeChat via Tencent ilink bot API (QR login, direct chat).',\n order: 3,\n deferConnectUntilAfterListen: true,\n } as const;\n\n readonly capabilities: ChannelCapabilities = {\n chatTypes: ['direct'] as ChatType[],\n reactions: false,\n threads: false,\n media: true,\n polls: false,\n nativeCommands: false,\n blockStreaming: true,\n };\n\n readonly configSchema = {\n schema: {},\n validate: (raw: unknown) => {\n const r = WeixinConfigSchema.safeParse(raw);\n return r.success ? { ok: true as const } : { ok: false as const, errors: [r.error.message] };\n },\n };\n\n readonly cronDelivery: ChannelCronDeliveryAdapter = {\n async normalizeDeliveryTarget(to, sessionStore) {\n const { chatId, accountId } = await normalizeWeixinCronDeliveryToResolved(to, sessionStore);\n return { chatId, accountId };\n },\n };\n\n readonly onboard = weixinOnboardAdapter;\n\n readonly cliLogin: ChannelCliLoginAdapter = {\n async runLogin(params) {\n const { runWeixinQrLoginCli } = await import('./cli/qr-login.js');\n return runWeixinQrLoginCli({\n configPath: params.configPath,\n verbose: params.verbose,\n timeoutMs: params.timeoutMs,\n account: params.accountId,\n writeConfig: params.writeConfig,\n });\n },\n };\n\n readonly configSurface = weixinConfigSurface;\n\n readonly defaults: ChannelPluginDefaults = {\n queue: { debounceMs: 0 },\n outbound: { textChunkLimit: 4000 },\n streaming: {\n blockStreamingCoalesce: {\n minChars: 200,\n idleMs: 3000,\n },\n },\n };\n\n private bus!: ChannelPluginInitOptions['bus'];\n private cfg!: Config;\n private abortControllers = new Map<string, AbortController>();\n\n config = {\n listAccountIds: (cfg: Config) => listWeixinAccountIds(cfg),\n resolveAccount: (cfg: Config, accountId?: string | null) => resolveWeixinAccount(cfg, accountId),\n isConfigured: async (account: ResolvedWeixinAccount) => account.configured,\n describeAccount: (account: ResolvedWeixinAccount, _cfg: Config) => ({\n accountId: account.accountId,\n channelId: 'weixin',\n name: account.name,\n enabled: account.enabled,\n configured: account.configured,\n status: undefined,\n }),\n };\n\n security = {\n resolveDmPolicy: ({ account }: { account: ResolvedWeixinAccount }) =>\n resolveDmPolicy(account.dmPolicy, 'open'),\n checkAccess: (ctx: ChannelSecurityContext, account: ResolvedWeixinAccount, _cfg: Config) => {\n const allowFrom = [...(account.allowFrom ?? []), ...readFrameworkAllowFromList(account.accountId)];\n return evaluateAccess({\n context: {\n channel: 'weixin',\n accountId: account.accountId,\n chatId: ctx.chatId,\n senderId: ctx.senderId,\n senderName: ctx.senderName,\n isGroup: false,\n isDm: true,\n },\n dmPolicy: account.dmPolicy,\n allowFrom,\n });\n },\n };\n\n outbound = {\n deliveryMode: 'direct' as const,\n chunker: weixinTextChunker,\n chunkerMode: 'text' as const,\n textChunkLimit: 4000,\n ...createWeixinOutboundHandlers(),\n };\n\n streaming: ChannelStreamingAdapter = {\n startStream: () => null,\n };\n\n /** Channel plugin hints (cron targets, media paths). */\n agentPrompt = {\n augmentSystemPrompt: (): string =>\n [\n 'Weixin (ilink): direct chat only. To send an image or file, use the message tool with action send and set media to a local absolute path or an HTTPS URL; relative paths may fail.',\n 'For cron or scheduled delivery to a Weixin contact, set delivery.to to the user Weixin id (ending in @im.wechat) and delivery.accountId to the bot account id, or outbound may pick the wrong account.',\n 'When using MEDIA: to attach a file, put the MEDIA: line alone on its own line, not inline with other text.',\n ].join('\\n'),\n };\n\n async init(options: ChannelPluginInitOptions): Promise<void> {\n this.bus = options.bus;\n this.cfg = options.config;\n log.debug('Weixin plugin initialized');\n }\n\n async start(options?: ChannelPluginStartOptions): Promise<void> {\n const ids = options?.accountId\n ? [options.accountId]\n : listWeixinAccountIds(this.cfg);\n\n for (const accountId of ids) {\n const account = resolveWeixinAccount(this.cfg, accountId);\n if (!account.enabled || !account.configured || !account.token) continue;\n\n if (this.abortControllers.has(accountId)) continue;\n\n restoreContextTokens(account.accountId);\n\n const ac = new AbortController();\n this.abortControllers.set(accountId, ac);\n\n void monitorWeixinProvider({\n account,\n config: this.cfg,\n bus: this.bus,\n abortSignal: ac.signal,\n }).catch((err) => {\n if ((err as { name?: string; message?: string } | undefined)?.name === 'AbortError') {\n log.debug({ accountId }, 'Weixin monitor stopped');\n return;\n }\n log.error({ err, accountId }, 'Weixin monitor exited with error');\n });\n\n log.info({ accountId }, 'Weixin monitor started');\n }\n }\n\n async stop(accountId?: string): Promise<void> {\n const ids = accountId ? [accountId] : [...this.abortControllers.keys()];\n for (const id of ids) {\n const ac = this.abortControllers.get(id);\n if (ac) {\n ac.abort();\n this.abortControllers.delete(id);\n }\n }\n }\n\n channelIsRunning(cfg: Config): boolean {\n return listWeixinAccountIds(cfg).some((id) => {\n const a = resolveWeixinAccount(cfg, id);\n return a.configured && a.enabled !== false && this.abortControllers.has(id);\n });\n }\n\n async onConfigUpdated(cfg: Config): Promise<void> {\n const prevWx = this.cfg.channels?.weixin as unknown;\n const nextWx = cfg.channels?.weixin as { enabled?: boolean } | undefined;\n const channelOff = !nextWx || nextWx.enabled !== true;\n\n if (channelOff) {\n this.cfg = cfg;\n await this.stop();\n return;\n }\n\n this.cfg = cfg;\n\n if (isDeepStrictEqual(prevWx, nextWx) && this.channelIsRunning(cfg)) {\n return;\n }\n\n await this.stop();\n await this.start();\n }\n\n /**\n * Restart long-poll monitors after credentials were written to disk without a `channels.weixin` JSON\n * delta (e.g. only token files / account index updated). Gateway calls this after QR login completes.\n *\n * Pass `bus` explicitly: if Weixin was disabled at gateway boot, `init()` was skipped and `this.bus` was never set.\n */\n async reloadMonitorsWithConfig(cfg: Config, bus: MessageBus): Promise<void> {\n this.bus = bus;\n this.cfg = cfg;\n await this.stop();\n await this.start();\n }\n}\n\nexport const weixinPlugin = new WeixinChannelPlugin();\n"],"mappings":"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"}
|