@cubeai/dsh-feishu-channels 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/manager.js CHANGED
@@ -26,7 +26,7 @@ function normalizeSendMode(input) {
26
26
  return input === 'steer' ? 'steer' : 'queue';
27
27
  }
28
28
  function channelAutoPrefix(platform) {
29
- return `${CHANNEL_META[platform].label}账号`;
29
+ return `${CHANNEL_META[platform].label}频道`;
30
30
  }
31
31
  function parseAutoOrdinal(platform, name) {
32
32
  const trimmed = String(name ?? '').trim();
@@ -143,7 +143,8 @@ export class ChannelManager {
143
143
  this.applyAssistant(this.store.assistant);
144
144
  this.applyWorkspace(this.store.cwd);
145
145
  this.applyPermission(this.store.permission);
146
- this.migrateAccountSettings();
146
+ if (this.migrateAccountSettings())
147
+ this.flush();
147
148
  const seen = new SeenStore(join(options.stateDir, 'seen.json'));
148
149
  const credentials = options.ctx.credentials;
149
150
  this.vault = credentials
@@ -223,7 +224,6 @@ export class ChannelManager {
223
224
  nameOrdinal,
224
225
  appId: typeof config.appId === 'string' ? config.appId : '',
225
226
  hasAppSecret: Boolean(config.appSecretRef || config.appSecret),
226
- provisioning: config.provisioning,
227
227
  connected,
228
228
  connectionState: runtimeState,
229
229
  receiveConfigured: state.receiveEnabled !== false,
@@ -305,10 +305,9 @@ export class ChannelManager {
305
305
  }),
306
306
  timeout,
307
307
  ]);
308
- const current = this.store.channels[accountId];
309
- if (current) {
310
- current.config = { ...(current.config ?? {}), provisioning };
311
- this.flush();
308
+ const provisioningErrors = Array.isArray(provisioning?.errors) ? provisioning.errors : [];
309
+ if (provisioningErrors.length > 0) {
310
+ this.log(`[manager] 应用配置初始化未完成: ${provisioningErrors.map((item) => item?.message ?? item?.step ?? 'unknown').join('; ')}`);
312
311
  }
313
312
  }
314
313
  }
@@ -323,13 +322,13 @@ export class ChannelManager {
323
322
  }
324
323
  catch (error) {
325
324
  this.log(`[manager] ${id} 连接失败: ${error instanceof Error ? error.message : String(error)}`);
326
- return { ok: false, error: '账号连接失败,请查看本机日志', accountId, created: !existed, newIdentity: hadAnotherIdentity };
325
+ return { ok: false, error: '频道连接失败,请查看本机日志', accountId, created: !existed, newIdentity: hadAnotherIdentity };
327
326
  }
328
327
  }
329
328
  async setReceive(id, receiveEnabled) {
330
329
  const accountId = this.resolveAccountId(id);
331
330
  if (!accountId)
332
- return { ok: false, error: '账号不存在' };
331
+ return { ok: false, error: '频道不存在' };
333
332
  return this.channelOperations.run(accountId, () => this.setReceiveNow(accountId, receiveEnabled));
334
333
  }
335
334
  async setReceiveNow(id, receiveEnabled) {
@@ -571,7 +570,7 @@ export class ChannelManager {
571
570
  }
572
571
  const accountId = this.resolveAccountId(id);
573
572
  if (!accountId) {
574
- send(404, { ok: false, error: '账号不存在或该渠道包含多个账号' });
573
+ send(404, { ok: false, error: '频道不存在或该渠道包含多个频道' });
575
574
  return;
576
575
  }
577
576
  if (action === 'approve')
@@ -589,7 +588,7 @@ export class ChannelManager {
589
588
  const action = parts[4];
590
589
  const body = await readBody();
591
590
  if (!this.store.channels[accountId]) {
592
- send(404, { ok: false, error: '账号不存在' });
591
+ send(404, { ok: false, error: '频道不存在' });
593
592
  return;
594
593
  }
595
594
  if (action === 'settings') {
@@ -622,8 +621,6 @@ export class ChannelManager {
622
621
  name: state.name,
623
622
  log: this.log,
624
623
  });
625
- state.config = { ...(state.config ?? {}), provisioning };
626
- this.flush();
627
624
  send(200, { ok: true, account: this.accountView(accountId, state), provisioning });
628
625
  return;
629
626
  }
@@ -657,7 +654,7 @@ export class ChannelManager {
657
654
  send(200, { ok: true, pending: this.pendingRequests() });
658
655
  return;
659
656
  }
660
- send(404, { ok: false, error: `未知账号操作 ${action}` });
657
+ send(404, { ok: false, error: `未知频道操作 ${action}` });
661
658
  return;
662
659
  }
663
660
  send(404, { ok: false, error: 'not found' });
@@ -729,11 +726,11 @@ export class ChannelManager {
729
726
  }
730
727
  async updateAccount(accountId, input) {
731
728
  if (!this.store.channels[accountId])
732
- return { ok: false, error: '账号不存在' };
729
+ return { ok: false, error: '频道不存在' };
733
730
  return this.channelOperations.run(accountId, async () => {
734
731
  const state = this.store.channels[accountId];
735
732
  if (!state)
736
- return { ok: false, error: '账号不存在' };
733
+ return { ok: false, error: '频道不存在' };
737
734
  const platform = this.platformOf(accountId, state);
738
735
  const normalized = await this.normalizeAccountSettings(platform, input, state);
739
736
  if (!normalized.ok)
@@ -774,7 +771,7 @@ export class ChannelManager {
774
771
  const job = this.channelOperations.run(accountId, async () => {
775
772
  const state = this.store.channels[accountId];
776
773
  if (!state)
777
- return { ok: false, error: '账号不存在' };
774
+ return { ok: false, error: '频道不存在' };
778
775
  if (this.disposed)
779
776
  return { ok: false, error: '操作失败,请查看本机日志' };
780
777
  const signal = AbortSignal.any([AbortSignal.timeout(12_000), this.diagnosticAbort.signal]);
@@ -824,7 +821,7 @@ export class ChannelManager {
824
821
  }
825
822
  async reconnect(accountId) {
826
823
  if (!this.store.channels[accountId])
827
- return { ok: false, error: '账号不存在' };
824
+ return { ok: false, error: '频道不存在' };
828
825
  return this.channelOperations.run(accountId, async () => {
829
826
  try {
830
827
  const state = this.store.channels[accountId];
@@ -835,7 +832,7 @@ export class ChannelManager {
835
832
  }
836
833
  catch (error) {
837
834
  this.log(`[manager] 重新连接 ${accountId} 失败: ${error instanceof Error ? error.message : String(error)}`);
838
- return { ok: false, error: '重新连接失败,请查看本机日志' };
835
+ return { ok: false, error: '重新连接频道失败,请查看本机日志' };
839
836
  }
840
837
  });
841
838
  }
@@ -980,7 +977,7 @@ export class ChannelManager {
980
977
  return;
981
978
  const state = this.store.channels[id];
982
979
  if (!state)
983
- throw new Error('账号不存在');
980
+ throw new Error('频道不存在');
984
981
  const platform = this.platformOf(id, state);
985
982
  const resolved = await this.resolveSecrets(platform, id, state.config ?? {});
986
983
  if (this.disposed)
@@ -993,7 +990,7 @@ export class ChannelManager {
993
990
  try {
994
991
  adapter = createChannelAdapter(platform, resolved, this.log, accountDir, {
995
992
  accountId: id,
996
- accountLabel: state.name || `${CHANNEL_META[platform].label}账号`,
993
+ accountLabel: state.name || `${CHANNEL_META[platform].label}频道`,
997
994
  host: this.ctx,
998
995
  });
999
996
  if (!adapter)
@@ -1104,11 +1101,18 @@ export class ChannelManager {
1104
1101
  const assistant = this.currentAssistant() ?? { provider: this.engineConfig.provider, model: this.engineConfig.model };
1105
1102
  const cwd = this.currentWorkspace();
1106
1103
  const permission = this.currentPermission();
1104
+ let removedProvisioning = false;
1107
1105
  for (const [id, state] of Object.entries(this.store.channels)) {
1108
1106
  const platform = this.platformOf(id, state);
1109
1107
  state.id = id;
1110
1108
  state.platform = platform;
1111
- state.name = String(state.name || `${CHANNEL_META[platform].label}账号`).trim();
1109
+ state.name = String(state.name || `${CHANNEL_META[platform].label}频道`).trim();
1110
+ if (state.config && typeof state.config === 'object' && Object.prototype.hasOwnProperty.call(state.config, 'provisioning')) {
1111
+ const nextConfig = { ...state.config };
1112
+ delete nextConfig.provisioning;
1113
+ state.config = nextConfig;
1114
+ removedProvisioning = true;
1115
+ }
1112
1116
  state.assistant = normalizeAssistantModel(state.assistant ?? {}) ?? assistant;
1113
1117
  state.cwd = normalizeWorkspacePath(state.cwd) ?? cwd;
1114
1118
  state.permission = normalizePermission(state.permission, this.permissionPresets().names) ?? permission;
@@ -1116,6 +1120,7 @@ export class ChannelManager {
1116
1120
  state.sendMode = normalizeSendMode(state.sendMode);
1117
1121
  }
1118
1122
  this.store.version = 2;
1123
+ return removedProvisioning;
1119
1124
  }
1120
1125
  async clearUnsupportedReasoningEfforts() {
1121
1126
  const llm = this.ctx.get?.('llm');
@@ -1197,9 +1202,9 @@ export class ChannelManager {
1197
1202
  const requested = input.name !== undefined ? String(input.name) : String(previous.name ?? '');
1198
1203
  const trimmed = requested.trim();
1199
1204
  if ([...trimmed].length > ACCOUNT_REMARK_LIMIT)
1200
- return { ok: false, error: `账号备注不能超过 ${ACCOUNT_REMARK_LIMIT} 个字符` };
1205
+ return { ok: false, error: `频道名称不能超过 ${ACCOUNT_REMARK_LIMIT} 个字符` };
1201
1206
  if (/[\u0000-\u001F\u007F]/.test(trimmed))
1202
- return { ok: false, error: '账号备注不能包含换行或控制字符' };
1207
+ return { ok: false, error: '频道名称不能包含换行或控制字符' };
1203
1208
  const name = trimmed || autoAccountName(platform, nameOrdinal);
1204
1209
  return { ok: true, settings: { name, nameOrdinal, assistant, agentPreset, cwd, permission, privateAccess, sendMode, commandPermissions } };
1205
1210
  }
@@ -1,112 +0,0 @@
1
- # Sidebar coexistence with dsh-im-connect
2
-
3
- This document is the maintenance contract for the shared "任务 | 频道 | 飞书频道" tab bar.
4
-
5
- ## Why a contract is needed
6
-
7
- DSH has no official sidebar-tab slot. `sidebar.workspaces` is a single slot owned by
8
- `@deepseek-ai/dsh-client-ui-workspace`. `dsh-im-connect` wraps that component and
9
- attaches a private **Registry v1** (`component.__dshNativeTabs`). Other plugins can insert
10
- tabs into that registry without modifying `dsh-im-connect`.
11
-
12
- `@cubeai/dsh-feishu-channels` implements the same registry contract. Either plugin may be
13
- the wrapper ("host"); the other one inserts its tab.
14
-
15
- ## Registry v1
16
-
17
- ```ts
18
- interface SidebarTabRegistryV1 {
19
- version: 1
20
- officialTree: React.ComponentType<any>
21
- sessionFilters: Array<(sessionId: string) => boolean>
22
- sessionOpeners: Array<{ id: string, order?: number, matchSession(id: string): boolean, open(id: string, hostOpen?: (id: string) => void): void }>
23
- getTabs(): Tab[]
24
- getRevision(): number
25
- subscribe(listener: () => void): () => void
26
- insert(tab: Tab): () => void
27
- addSessionFilter(filter: (id: string) => boolean): () => void
28
- addSessionOpener(opener: SessionOpener): () => void
29
- }
30
- ```
31
-
32
- Discovery rules (compatible with `dsh-im-connect@1.0.12`):
33
-
34
- 1. Read `sidebar.workspaces` occupants.
35
- 2. If `occupant.component.__dshNativeTabs` is a valid v1 registry, insert a tab; do not wrap.
36
- 3. Else if the component is already wrapped by an unknown host, leave it untouched.
37
- 4. Else wrap the official component and attach a registry to the wrapper function.
38
-
39
- Wrapper markers:
40
-
41
- - `__dshNativeTabHost = true`
42
- - `__dshNativeTabWrapped = true`
43
- - `__dshNativeTabOriginal = originalComponent`
44
- - `__dshNativeTabOwner = 'im-connect' | 'feishu-channel'`
45
- - `__dfcTabHost = true` (new plugin only)
46
-
47
- Coordination event: `window.dispatchEvent(new Event('dsh-native-sidebar-change'))`.
48
-
49
- ## Tabs
50
-
51
- | Plugin | id | order | Label |
52
- |---|---|---:|---|
53
- | dsh-im-connect | `channels` | 20 | 频道 / Channels |
54
- | dsh-feishu-channels | `feishu` | 30 | 飞书频道 / Feishu |
55
-
56
- Tasks is the fixed first tab. Sorted result:
57
-
58
- ```text
59
- 任务 | 频道 | 飞书频道
60
- ```
61
-
62
- When only one plugin is installed, the other tab is absent.
63
-
64
- ## Session visibility
65
-
66
- - Feishu sessions use `dfc:`; im-connect sessions use `im:`.
67
- - Each plugin adds a session filter to the shared registry so its own sessions do not appear
68
- in the Tasks tree.
69
- - A session id never matches both prefixes.
70
- - Do not share `sessions.json`, `channels.json`, credentials, or state directories.
71
-
72
- ## Lifecycle
73
-
74
- | Event | Behavior |
75
- |---|---|
76
- | New plugin loads after im-connect | Insert `feishu` into im-connect's registry |
77
- | im-connect loads after new plugin | im-connect detects the new registry and inserts `channels` |
78
- | New plugin unloads, im-connect remains | Remove own tab/filter; if own wrapper, restore official component and dispatch change event; im-connect re-wraps |
79
- | im-connect unloads, new plugin remains | im-connect removes its tab/restores; new plugin re-wraps on change event |
80
- | Both disabled | Official sidebar |
81
- | Page refresh | Deterministic re-evaluation from the bundle order |
82
-
83
- All disposers must be idempotent and remove only their own tab/filter/opener.
84
-
85
- ## Known limitations
86
-
87
- - Registry v1 is a private convention, not a DSH public API. A future im-connect release may
88
- change it; the new plugin degrades to the official sidebar rather than fighting it.
89
- - Codex UI sidebars (`sidebar.channels`) are not wrapped by v1.
90
- - Two plugins must not use the same Feishu App ID at the same time; Feishu long-connection
91
- events are delivered to one connection.
92
- - **Uninstall order:** because both plugins share the single `sidebar.workspaces` slot and
93
- one wraps the other, uninstalling `dsh-im-connect` while the new plugin is active can be
94
- rejected by DSH's `bundle-in-use` guard. Disable `@cubeai/dsh-feishu-channels` first, then
95
- uninstall `dsh-im-connect`; the new plugin can be re-enabled afterwards (it falls back to
96
- the official sidebar and shows `任务 | 飞书频道`). This is a UI-wrapper/runtime coupling,
97
- not an npm dependency: neither package imports the other, and their Host state, APIs,
98
- credentials, and sessions are separate.
99
-
100
- ## Verification matrix
101
-
102
- | Scenario | Expected |
103
- |---|---|
104
- | new plugin only | 任务 \| 飞书频道 |
105
- | im-connect only | 任务 \| 频道 |
106
- | im-connect first, new plugin second | 任务 \| 频道 \| 飞书频道 |
107
- | new plugin first, im-connect second | 任务 \| 频道 \| 飞书频道 |
108
- | disable new plugin | 任务 \| 频道 |
109
- | disable im-connect | 任务 \| 飞书频道 |
110
- | uninstall either | Disable the other plugin first if DSH reports `bundle-in-use`; then uninstall, then re-enable |
111
- | refresh | Same as before refresh |
112
- | reload client | No duplicate tabs |