@cubeai/dsh-feishu-channels 0.2.0 → 0.2.3
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/README.md +16 -16
- package/client.js +36 -36
- package/cordis.patch.yml +1 -1
- package/docs/dsh-feishu-channels.md +538 -0
- package/locale/zh.json +1 -1
- package/package.json +2 -2
- package/src/channels/feishu-app.js +2 -0
- package/src/channels/qr/feishu.js +1 -1
- package/src/channels/qr/hub.js +2 -2
- package/src/engine/chat-commands.js +4 -4
- package/src/engine/command-messages.js +3 -3
- package/src/engine/gateway.js +1 -1
- package/src/manager.js +29 -24
- package/docs/coexistence.md +0 -112
- package/docs/dsh-feishu-channels-plan.md +0 -1406
- package/docs/ui-revamp-plan.md +0 -263
- package/docs/verification.md +0 -120
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
|
|
309
|
-
if (
|
|
310
|
-
|
|
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: '
|
|
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:
|
|
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}
|
|
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:
|
|
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
|
}
|
package/docs/coexistence.md
DELETED
|
@@ -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 |
|