dsh-connect 0.8.0 → 0.9.2
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.i18n.yaml +2 -2
- package/README.md +236 -18
- package/README.zh.md +185 -18
- package/client/client.js +517 -84
- package/client/client.js.map +4 -4
- package/client/locale.mjs +227 -0
- package/client/panel-state.mjs +54 -0
- package/client/settings-client.mjs +282 -61
- package/docs/images/settings-advanced-en.png +0 -0
- package/docs/images/settings-advanced-zh.png +0 -0
- package/docs/images/settings-defaults-en.png +0 -0
- package/docs/images/settings-defaults-zh.png +0 -0
- package/docs/images/settings-overview-en.png +0 -0
- package/docs/images/settings-overview-zh.png +0 -0
- package/lib/binding.d.ts.map +1 -1
- package/lib/binding.js +2 -2
- package/lib/binding.js.map +1 -1
- package/lib/channels/dingtalk/index.d.ts +49 -49
- package/lib/channels/dingtalk/index.d.ts.map +1 -1
- package/lib/channels/dingtalk/message.d.ts.map +1 -1
- package/lib/channels/dingtalk/message.js +7 -0
- package/lib/channels/dingtalk/message.js.map +1 -1
- package/lib/channels/feishu/adapter.d.ts +14 -13
- package/lib/channels/feishu/adapter.d.ts.map +1 -1
- package/lib/channels/feishu/adapter.js +122 -56
- package/lib/channels/feishu/adapter.js.map +1 -1
- package/lib/channels/feishu/i18n.d.ts +2 -0
- package/lib/channels/feishu/i18n.d.ts.map +1 -1
- package/lib/channels/feishu/i18n.js +2 -0
- package/lib/channels/feishu/i18n.js.map +1 -1
- package/lib/channels/feishu/index.d.ts +48 -27
- package/lib/channels/feishu/index.d.ts.map +1 -1
- package/lib/channels/feishu/index.js +32 -6
- package/lib/channels/feishu/index.js.map +1 -1
- package/lib/channels/telegram/adapter.d.ts.map +1 -1
- package/lib/channels/telegram/adapter.js +7 -1
- package/lib/channels/telegram/adapter.js.map +1 -1
- package/lib/channels/telegram/index.d.ts +13 -13
- package/lib/channels/telegram/index.d.ts.map +1 -1
- package/lib/channels/web/adapter.d.ts +3 -1
- package/lib/channels/web/adapter.d.ts.map +1 -1
- package/lib/channels/web/adapter.js +3 -1
- package/lib/channels/web/adapter.js.map +1 -1
- package/lib/channels/web/index.d.ts +5 -5
- package/lib/channels/web/index.d.ts.map +1 -1
- package/lib/chat-key.d.ts +29 -0
- package/lib/chat-key.d.ts.map +1 -0
- package/lib/chat-key.js +38 -0
- package/lib/chat-key.js.map +1 -0
- package/lib/index.d.ts +57 -63
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +330 -55
- package/lib/index.js.map +1 -1
- package/lib/interaction.d.ts +90 -50
- package/lib/interaction.d.ts.map +1 -1
- package/lib/interaction.js +233 -272
- package/lib/interaction.js.map +1 -1
- package/lib/retry.d.ts.map +1 -1
- package/lib/retry.js +7 -1
- package/lib/retry.js.map +1 -1
- package/lib/runner.d.ts +54 -0
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +158 -23
- package/lib/runner.js.map +1 -1
- package/lib/scheduler.d.ts.map +1 -1
- package/lib/scheduler.js +2 -2
- package/lib/scheduler.js.map +1 -1
- package/lib/service.d.ts +16 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +46 -4
- package/lib/service.js.map +1 -1
- package/lib/settings/channel-runtime.d.ts +75 -0
- package/lib/settings/channel-runtime.d.ts.map +1 -0
- package/lib/settings/channel-runtime.js +165 -0
- package/lib/settings/channel-runtime.js.map +1 -0
- package/lib/settings/channels.d.ts +11 -0
- package/lib/settings/channels.d.ts.map +1 -1
- package/lib/settings/channels.js +31 -0
- package/lib/settings/channels.js.map +1 -1
- package/lib/settings/credential-store.d.ts +52 -6
- package/lib/settings/credential-store.d.ts.map +1 -1
- package/lib/settings/credential-store.js +92 -18
- package/lib/settings/credential-store.js.map +1 -1
- package/lib/settings/legacy-import.d.ts +190 -0
- package/lib/settings/legacy-import.d.ts.map +1 -0
- package/lib/settings/legacy-import.js +373 -0
- package/lib/settings/legacy-import.js.map +1 -0
- package/lib/settings/namespace.d.ts +263 -0
- package/lib/settings/namespace.d.ts.map +1 -0
- package/lib/settings/namespace.js +287 -0
- package/lib/settings/namespace.js.map +1 -0
- package/lib/settings/secret-disclosure.d.ts +50 -0
- package/lib/settings/secret-disclosure.d.ts.map +1 -0
- package/lib/settings/secret-disclosure.js +132 -0
- package/lib/settings/secret-disclosure.js.map +1 -0
- package/lib/settings/settings-model.d.ts +17 -0
- package/lib/settings/settings-model.d.ts.map +1 -1
- package/lib/settings/settings-model.js +18 -1
- package/lib/settings/settings-model.js.map +1 -1
- package/lib/settings/settings-rpc.d.ts +71 -3
- package/lib/settings/settings-rpc.d.ts.map +1 -1
- package/lib/settings/settings-rpc.js +167 -24
- package/lib/settings/settings-rpc.js.map +1 -1
- package/lib/settings/settings-service.d.ts +22 -1
- package/lib/settings/settings-service.d.ts.map +1 -1
- package/lib/settings/settings-service.js +145 -8
- package/lib/settings/settings-service.js.map +1 -1
- package/lib/state-dir.d.ts +28 -0
- package/lib/state-dir.d.ts.map +1 -0
- package/lib/state-dir.js +33 -0
- package/lib/state-dir.js.map +1 -0
- package/lib/stream.d.ts.map +1 -1
- package/lib/stream.js +3 -2
- package/lib/stream.js.map +1 -1
- package/lib/types.d.ts +14 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +13 -10
package/lib/index.d.ts
CHANGED
|
@@ -37,11 +37,21 @@ export * as settings from "./settings/index.js";
|
|
|
37
37
|
/** Cordis plugin name used by loader diagnostics. */
|
|
38
38
|
export declare const name = "connect";
|
|
39
39
|
/**
|
|
40
|
-
* Host-plane services this plugin must wait for before `apply` runs. All
|
|
41
|
-
* are
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* Host-plane services this plugin must wait for before `apply` runs. All four
|
|
41
|
+
* are rows in the always-loaded `@deepseek-ai/dsh-base` bundle — the shared core
|
|
42
|
+
* of every base-backed profile — so requiring them cannot stall the plugin in
|
|
43
|
+
* any profile that can load `dsh-connect` at all.
|
|
44
|
+
*
|
|
45
|
+
* `credentials` must be listed here, not read lazily inside `apply`: a
|
|
46
|
+
* `ctx.get("credentials")` at apply time returns `undefined`, because this
|
|
47
|
+
* plugin activates before the credentials row does. That silently disabled the
|
|
48
|
+
* whole credential path — `migrateConfigSecrets` never seeded the store (so an
|
|
49
|
+
* upgraded user's working channels displayed "未配置凭据"), `injectSecrets`
|
|
50
|
+
* never merged store-backed secrets into a channel config, and
|
|
51
|
+
* `settings.saveCredentials` threw `not-configured` for every pane save. Note
|
|
52
|
+
* cordis v4 has no `?` optional-inject marker, so `"credentials?"` would be read
|
|
53
|
+
* as a required service literally named `"credentials?"` — that is a syntax
|
|
54
|
+
* hazard, not a reason the service itself is unavailable.
|
|
45
55
|
*/
|
|
46
56
|
export declare const inject: string[];
|
|
47
57
|
/**
|
|
@@ -50,89 +60,73 @@ export declare const inject: string[];
|
|
|
50
60
|
* web-settings state path. Each channel's own `Config` schema is also exported
|
|
51
61
|
* (as `feishu.Config`, etc.) for reference; the merged entry validates loosely.
|
|
52
62
|
*/
|
|
53
|
-
export declare const Config: z<Schemastery.ObjectS<{
|
|
63
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
64
|
+
/** Optional path to persist web-settings edits (non-secret). */
|
|
65
|
+
settingsStatePath: z<string, string, "plain">;
|
|
54
66
|
/** Agent preset id composed into each bound session; omit for the roster default. */
|
|
55
|
-
agentPreset: z<string, string>;
|
|
67
|
+
agentPreset: z<string, string, "plain">;
|
|
56
68
|
/** Absolute working directory per agent; defaults to the process cwd. */
|
|
57
|
-
workDir: z<string, string>;
|
|
69
|
+
workDir: z<string, string, "plain">;
|
|
58
70
|
/** Optional workspace directories offered by the `/dir` chooser. */
|
|
59
|
-
workspaces: z<string[], string[]>;
|
|
71
|
+
workspaces: z<string[], string[], "plain">;
|
|
60
72
|
/** Vision-capable model used to describe images when the main model can't see them. */
|
|
61
|
-
visionModel: z<Schemastery.ObjectS<{
|
|
62
|
-
provider: z<string, string>;
|
|
63
|
-
model: z<string, string>;
|
|
64
|
-
}
|
|
65
|
-
provider: z<string, string>;
|
|
66
|
-
model: z<string, string>;
|
|
67
|
-
}
|
|
73
|
+
visionModel: z<Schemastery.ObjectS<NoInfer<{
|
|
74
|
+
provider: z<string, string, "plain">;
|
|
75
|
+
model: z<string, string, "plain">;
|
|
76
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
77
|
+
provider: z<string, string, "plain">;
|
|
78
|
+
model: z<string, string, "plain">;
|
|
79
|
+
}>>, "plain">;
|
|
68
80
|
/** User-facing message language: `zh` (default) or `en`. */
|
|
69
|
-
language: z<"zh" | "en", "zh" | "en">;
|
|
81
|
+
language: z<"zh" | "en", "zh" | "en", "plain">;
|
|
70
82
|
/** Sender allowlist; empty = all senders allowed. */
|
|
71
|
-
allowUsers: z<string[], string[]>;
|
|
83
|
+
allowUsers: z<string[], string[], "plain">;
|
|
72
84
|
/** Chat allowlist; empty = all chats allowed. */
|
|
73
|
-
allowChats: z<string[], string[]>;
|
|
85
|
+
allowChats: z<string[], string[], "plain">;
|
|
74
86
|
/** Directory for the bindings.json routing store. */
|
|
75
|
-
stateDir: z<string, string>;
|
|
87
|
+
stateDir: z<string, string, "plain">;
|
|
76
88
|
/** Automatically create a Web mirror for new sessions (default: true). */
|
|
77
|
-
autoMirror: z<boolean, boolean>;
|
|
89
|
+
autoMirror: z<boolean, boolean, "plain">;
|
|
78
90
|
/** Liveness heartbeat interval ms for the streaming card; 0 disables it (default: 60000). */
|
|
79
|
-
streamHeartbeatMs: z<number, number>;
|
|
91
|
+
streamHeartbeatMs: z<number, number, "plain">;
|
|
80
92
|
/** Default notification level for streaming replies. Default 'result' (final answer only) keeps cards short. */
|
|
81
|
-
notifyLevel: z<"full" | "important" | "result", "full" | "important" | "result">;
|
|
93
|
+
notifyLevel: z<"full" | "important" | "result", "full" | "important" | "result", "plain">;
|
|
82
94
|
/** Proactive progress-notice interval ms when a turn stays silent (default: 300000 = 5 min; 0 disables). */
|
|
83
|
-
progressTimeoutMs: z<number, number>;
|
|
84
|
-
|
|
85
|
-
channels: z<("feishu" | "web" | "telegram" | "dingtalk")[], ("feishu" | "web" | "telegram" | "dingtalk")[]>;
|
|
86
|
-
/** Keys applied to every channel that doesn't set its own (e.g. a shared `language`). */
|
|
87
|
-
channelDefaults: z<any, any>;
|
|
88
|
-
feishu: z<any, any>;
|
|
89
|
-
telegram: z<any, any>;
|
|
90
|
-
dingtalk: z<any, any>;
|
|
91
|
-
web: z<any, any>;
|
|
95
|
+
progressTimeoutMs: z<number, number, "plain">;
|
|
96
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
92
97
|
/** Optional path to persist web-settings edits (non-secret). */
|
|
93
|
-
settingsStatePath: z<string, string>;
|
|
94
|
-
}>, Schemastery.ObjectT<{
|
|
98
|
+
settingsStatePath: z<string, string, "plain">;
|
|
95
99
|
/** Agent preset id composed into each bound session; omit for the roster default. */
|
|
96
|
-
agentPreset: z<string, string>;
|
|
100
|
+
agentPreset: z<string, string, "plain">;
|
|
97
101
|
/** Absolute working directory per agent; defaults to the process cwd. */
|
|
98
|
-
workDir: z<string, string>;
|
|
102
|
+
workDir: z<string, string, "plain">;
|
|
99
103
|
/** Optional workspace directories offered by the `/dir` chooser. */
|
|
100
|
-
workspaces: z<string[], string[]>;
|
|
104
|
+
workspaces: z<string[], string[], "plain">;
|
|
101
105
|
/** Vision-capable model used to describe images when the main model can't see them. */
|
|
102
|
-
visionModel: z<Schemastery.ObjectS<{
|
|
103
|
-
provider: z<string, string>;
|
|
104
|
-
model: z<string, string>;
|
|
105
|
-
}
|
|
106
|
-
provider: z<string, string>;
|
|
107
|
-
model: z<string, string>;
|
|
108
|
-
}
|
|
106
|
+
visionModel: z<Schemastery.ObjectS<NoInfer<{
|
|
107
|
+
provider: z<string, string, "plain">;
|
|
108
|
+
model: z<string, string, "plain">;
|
|
109
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
110
|
+
provider: z<string, string, "plain">;
|
|
111
|
+
model: z<string, string, "plain">;
|
|
112
|
+
}>>, "plain">;
|
|
109
113
|
/** User-facing message language: `zh` (default) or `en`. */
|
|
110
|
-
language: z<"zh" | "en", "zh" | "en">;
|
|
114
|
+
language: z<"zh" | "en", "zh" | "en", "plain">;
|
|
111
115
|
/** Sender allowlist; empty = all senders allowed. */
|
|
112
|
-
allowUsers: z<string[], string[]>;
|
|
116
|
+
allowUsers: z<string[], string[], "plain">;
|
|
113
117
|
/** Chat allowlist; empty = all chats allowed. */
|
|
114
|
-
allowChats: z<string[], string[]>;
|
|
118
|
+
allowChats: z<string[], string[], "plain">;
|
|
115
119
|
/** Directory for the bindings.json routing store. */
|
|
116
|
-
stateDir: z<string, string>;
|
|
120
|
+
stateDir: z<string, string, "plain">;
|
|
117
121
|
/** Automatically create a Web mirror for new sessions (default: true). */
|
|
118
|
-
autoMirror: z<boolean, boolean>;
|
|
122
|
+
autoMirror: z<boolean, boolean, "plain">;
|
|
119
123
|
/** Liveness heartbeat interval ms for the streaming card; 0 disables it (default: 60000). */
|
|
120
|
-
streamHeartbeatMs: z<number, number>;
|
|
124
|
+
streamHeartbeatMs: z<number, number, "plain">;
|
|
121
125
|
/** Default notification level for streaming replies. Default 'result' (final answer only) keeps cards short. */
|
|
122
|
-
notifyLevel: z<"full" | "important" | "result", "full" | "important" | "result">;
|
|
126
|
+
notifyLevel: z<"full" | "important" | "result", "full" | "important" | "result", "plain">;
|
|
123
127
|
/** Proactive progress-notice interval ms when a turn stays silent (default: 300000 = 5 min; 0 disables). */
|
|
124
|
-
progressTimeoutMs: z<number, number>;
|
|
125
|
-
|
|
126
|
-
channels: z<("feishu" | "web" | "telegram" | "dingtalk")[], ("feishu" | "web" | "telegram" | "dingtalk")[]>;
|
|
127
|
-
/** Keys applied to every channel that doesn't set its own (e.g. a shared `language`). */
|
|
128
|
-
channelDefaults: z<any, any>;
|
|
129
|
-
feishu: z<any, any>;
|
|
130
|
-
telegram: z<any, any>;
|
|
131
|
-
dingtalk: z<any, any>;
|
|
132
|
-
web: z<any, any>;
|
|
133
|
-
/** Optional path to persist web-settings edits (non-secret). */
|
|
134
|
-
settingsStatePath: z<string, string>;
|
|
135
|
-
}>>;
|
|
128
|
+
progressTimeoutMs: z<number, number, "plain">;
|
|
129
|
+
}>>, "plain">;
|
|
136
130
|
/** The merged plugin's config type (core fields + channel selector + settings). */
|
|
137
131
|
export interface ConnectSettingsConfig extends ConnectConfig {
|
|
138
132
|
/** Channels to activate; default: all built-in channels. */
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,CAAC,MAAM,0BAA0B,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/lib/index.js
CHANGED
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import z from "@deepseek-ai/schemastery";
|
|
15
15
|
import { ConnectService } from "./service.js";
|
|
16
|
-
import {
|
|
16
|
+
import { resolveStateDir } from "./state-dir.js";
|
|
17
|
+
import { CHANNELS, extractConfigSecrets, injectSecrets, } from "./settings/channels.js";
|
|
18
|
+
import { ChannelRuntime } from "./settings/channel-runtime.js";
|
|
19
|
+
import { CONNECT_PRESENTATION, installConnectSection, materializeConfig, paneConfigFields, sectionOf, } from "./settings/namespace.js";
|
|
20
|
+
import { importLegacySection } from "./settings/legacy-import.js";
|
|
17
21
|
import { installSettingsRpc } from "./settings/settings-rpc.js";
|
|
18
22
|
import { createSettingsService } from "./settings/settings-service.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { register as feishuRegister } from "./channels/feishu/index.js";
|
|
23
|
+
import { CHANNEL_SECRET_KEYS, createCredentialStore, } from "./settings/credential-store.js";
|
|
24
|
+
import { register as feishuRegister, loadCredentials } from "./channels/feishu/index.js";
|
|
22
25
|
import { register as telegramRegister } from "./channels/telegram/index.js";
|
|
23
26
|
import { register as dingtalkRegister } from "./channels/dingtalk/index.js";
|
|
24
27
|
import { register as webRegister } from "./channels/web/index.js";
|
|
@@ -44,16 +47,145 @@ export * as dingtalk from "./channels/dingtalk/index.js";
|
|
|
44
47
|
export * as web from "./channels/web/index.js";
|
|
45
48
|
// Web-settings stack (host RPC + credential store + persistence + client model).
|
|
46
49
|
export * as settings from "./settings/index.js";
|
|
50
|
+
/**
|
|
51
|
+
* Backward-compat: on boot, seed the credential store from secrets a channel
|
|
52
|
+
* config already carries (e.g. `feishu.appId`/`appSecret` written into the
|
|
53
|
+
* profile before the web-settings pane existed). Keeps the pane's presence
|
|
54
|
+
* reporting honest — an upgraded user whose channels work via config-file
|
|
55
|
+
* secrets sees them as "已配置" instead of "未配置凭据".
|
|
56
|
+
*
|
|
57
|
+
* Non-destructive by design: a ref the store already holds is never re-written
|
|
58
|
+
* (pane-saved credentials always win), and only refs present in the config
|
|
59
|
+
* (non-empty string, at their real flat/nested location) are migrated. Secrets
|
|
60
|
+
* still never reach the settings state file — they only move into the store.
|
|
61
|
+
*/
|
|
62
|
+
async function seedCredentialRefs(provider, secretKeys, secrets) {
|
|
63
|
+
for (const [configKey, ref] of Object.entries(secretKeys)) {
|
|
64
|
+
const value = secrets[configKey];
|
|
65
|
+
if (value === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
let alreadyConfigured = false;
|
|
68
|
+
try {
|
|
69
|
+
alreadyConfigured = (await provider.describe(ref)).configured === true;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
alreadyConfigured = false;
|
|
73
|
+
}
|
|
74
|
+
if (alreadyConfigured)
|
|
75
|
+
continue;
|
|
76
|
+
try {
|
|
77
|
+
await provider.set(ref, value);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Per-ref migration failure should never block activation.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fold credentials left behind by the legacy one-click onboarding file into the
|
|
86
|
+
* store. Feishu's onboarding predates the credential store and wrote its own
|
|
87
|
+
* JSON, which nothing but the adapter read — so a user who onboarded before the
|
|
88
|
+
* store existed had a working bot that the settings pane reported as
|
|
89
|
+
* 「未配置凭据」 forever. Migration is idempotent and never overwrites a value the
|
|
90
|
+
* store already holds.
|
|
91
|
+
*/
|
|
92
|
+
async function migrateOnboardedSecrets(provider, enabled) {
|
|
93
|
+
if (!enabled.includes("feishu"))
|
|
94
|
+
return;
|
|
95
|
+
let onboarded = null;
|
|
96
|
+
try {
|
|
97
|
+
onboarded = loadCredentials();
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (onboarded === null)
|
|
103
|
+
return;
|
|
104
|
+
await seedCredentialRefs(provider, CHANNEL_SECRET_KEYS.feishu, {
|
|
105
|
+
appId: onboarded.appId,
|
|
106
|
+
appSecret: onboarded.appSecret,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function migrateConfigSecrets(provider, cfg, enabled) {
|
|
110
|
+
for (const ch of enabled) {
|
|
111
|
+
const secretKeys = CHANNEL_SECRET_KEYS[ch] ?? {};
|
|
112
|
+
if (Object.keys(secretKeys).length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
const raw = cfg[ch];
|
|
115
|
+
const secrets = extractConfigSecrets(raw, ch, secretKeys);
|
|
116
|
+
if (Object.keys(secrets).length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
await seedCredentialRefs(provider, secretKeys, secrets);
|
|
119
|
+
}
|
|
120
|
+
await migrateOnboardedSecrets(provider, enabled);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The profile entry id this instance was loaded from (`connect`).
|
|
124
|
+
*
|
|
125
|
+
* This — not the package name — is what `SettingsForms.replace()` addresses:
|
|
126
|
+
* its own jsdoc calls the first argument "Profile entry id". It comes from the
|
|
127
|
+
* loader entry, which is attached to the fiber at runtime but *typed* only in
|
|
128
|
+
* `cordis-plugin-loader`'s `declare module` augmentation; that package is the
|
|
129
|
+
* host's loader, not a dependency of this plugin, so importing the type is not
|
|
130
|
+
* an option and the narrow cast below is the whole of the compromise. Returns
|
|
131
|
+
* `undefined` outside a loader (`new Context().plugin(...)` in a test), which
|
|
132
|
+
* `installConnectSection` reports and degrades on rather than failing.
|
|
133
|
+
*/
|
|
134
|
+
function entryId(ctx) {
|
|
135
|
+
const fiber = ctx.fiber;
|
|
136
|
+
const id = fiber?.entry?.options?.id;
|
|
137
|
+
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The Harness home as the host resolved it, when it told us.
|
|
141
|
+
*
|
|
142
|
+
* `profileContext.home` is the authoritative answer (it also covers a home
|
|
143
|
+
* configured outside the environment); `undefined` hands the question to
|
|
144
|
+
* `resolveHarnessHome()`, which repeats the `$DSH_HOME` → `~/.dsh` precedence on
|
|
145
|
+
* its own. The pre-0.2 settings document lived in the *home*, not in the
|
|
146
|
+
* profile directory, which is why this is not derived from the state dir.
|
|
147
|
+
*/
|
|
148
|
+
function harnessHome(ctx) {
|
|
149
|
+
const profile = ctx.get?.("profileContext");
|
|
150
|
+
return typeof profile?.home === "string" ? profile.home : undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The active profile's patch file — `~/.dsh/profiles/<profile>/cordis.patch.yml`.
|
|
154
|
+
*
|
|
155
|
+
* Same `profileContext` service as above (`profileContext.patchPath`), and the
|
|
156
|
+
* same reason for going through `ctx.get`: the field is the host's, not ours.
|
|
157
|
+
* The one-shot legacy import anchors its marker on this path, because the
|
|
158
|
+
* marker means "this profile entry has already consumed the legacy document" and
|
|
159
|
+
* a profile is the only unit that can move without that staying true.
|
|
160
|
+
*/
|
|
161
|
+
function profilePatchPath(ctx) {
|
|
162
|
+
const profile = ctx.get?.("profileContext");
|
|
163
|
+
return typeof profile?.patchPath === "string" ? profile.patchPath : undefined;
|
|
164
|
+
}
|
|
165
|
+
/** One-line error text for a warning line; `String()` for a thrown non-Error. */
|
|
166
|
+
function errorText(error) {
|
|
167
|
+
return error instanceof Error ? error.message : String(error);
|
|
168
|
+
}
|
|
47
169
|
/** Cordis plugin name used by loader diagnostics. */
|
|
48
170
|
export const name = "connect";
|
|
49
171
|
/**
|
|
50
|
-
* Host-plane services this plugin must wait for before `apply` runs. All
|
|
51
|
-
* are
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
172
|
+
* Host-plane services this plugin must wait for before `apply` runs. All four
|
|
173
|
+
* are rows in the always-loaded `@deepseek-ai/dsh-base` bundle — the shared core
|
|
174
|
+
* of every base-backed profile — so requiring them cannot stall the plugin in
|
|
175
|
+
* any profile that can load `dsh-connect` at all.
|
|
176
|
+
*
|
|
177
|
+
* `credentials` must be listed here, not read lazily inside `apply`: a
|
|
178
|
+
* `ctx.get("credentials")` at apply time returns `undefined`, because this
|
|
179
|
+
* plugin activates before the credentials row does. That silently disabled the
|
|
180
|
+
* whole credential path — `migrateConfigSecrets` never seeded the store (so an
|
|
181
|
+
* upgraded user's working channels displayed "未配置凭据"), `injectSecrets`
|
|
182
|
+
* never merged store-backed secrets into a channel config, and
|
|
183
|
+
* `settings.saveCredentials` threw `not-configured` for every pane save. Note
|
|
184
|
+
* cordis v4 has no `?` optional-inject marker, so `"credentials?"` would be read
|
|
185
|
+
* as a required service literally named `"credentials?"` — that is a syntax
|
|
186
|
+
* hazard, not a reason the service itself is unavailable.
|
|
55
187
|
*/
|
|
56
|
-
export const inject = ["agents", "sessions", "agentDefaultModel"];
|
|
188
|
+
export const inject = ["agents", "sessions", "agentDefaultModel", "credentials"];
|
|
57
189
|
/**
|
|
58
190
|
* Full plugin config: the channel-agnostic core fields plus the channel
|
|
59
191
|
* selector (`channels`), shared `channelDefaults`, per-channel config, and the
|
|
@@ -85,14 +217,16 @@ export const Config = z.object({
|
|
|
85
217
|
notifyLevel: z.union([z.const("full"), z.const("important"), z.const("result")]),
|
|
86
218
|
/** Proactive progress-notice interval ms when a turn stays silent (default: 300000 = 5 min; 0 disables). */
|
|
87
219
|
progressTimeoutMs: z.number(),
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Channels to activate, shared `channelDefaults`, and the per-channel options
|
|
222
|
+
* the settings pane edits. Spread from `paneConfigFields()` rather than
|
|
223
|
+
* restated: those are exactly the fields a profile entry is allowed to
|
|
224
|
+
* hot-commit, so declaring them here as `volatile` leaves is what turns a pane
|
|
225
|
+
* save into a live change instead of a remount (see `settings/namespace.ts`).
|
|
226
|
+
* Each channel's `appSecret`/`appId`/`botToken` stays *outside* this set on
|
|
227
|
+
* purpose — a volatile container would copy the secret into the profile.
|
|
228
|
+
*/
|
|
229
|
+
...paneConfigFields(),
|
|
96
230
|
/** Optional path to persist web-settings edits (non-secret). */
|
|
97
231
|
settingsStatePath: z.string(),
|
|
98
232
|
});
|
|
@@ -104,13 +238,23 @@ export const Config = z.object({
|
|
|
104
238
|
* `apply` (the fiber is still LOADING until this function resolves).
|
|
105
239
|
*/
|
|
106
240
|
export async function apply(ctx, config = {}) {
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
241
|
+
// `config` as the loader resolved it. The pane-editable field set is declared
|
|
242
|
+
// `volatile` (see `Config` above), so those fields arrive here as *references*
|
|
243
|
+
// (`{ get() }`) whose value the loader replaces in place, while ordinary
|
|
244
|
+
// fields arrive as plain data. Both are kept: every consumer below works on a
|
|
245
|
+
// materialized plain-data copy (`cfg`, and nothing mutates the caller's
|
|
246
|
+
// object — `materializeConfig` copies as it walks), and the references stay
|
|
247
|
+
// behind in `rawConfig` so a pane save can be read back without re-running
|
|
248
|
+
// `apply`.
|
|
249
|
+
const rawConfig = (config ?? {});
|
|
250
|
+
const cfg = materializeConfig(rawConfig);
|
|
110
251
|
const connect = new ConnectService(ctx, cfg);
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
252
|
+
// DSH credentials store: report presence + inject secrets into each channel
|
|
253
|
+
// config so store-backed secrets reach the adapter without the config file
|
|
254
|
+
// carrying them. `credentials` is a declared inject (see `inject` above), so
|
|
255
|
+
// it is guaranteed present by the time `apply` runs; the optional `ctx.get()`
|
|
256
|
+
// read is kept only so a bare context (unit tests) degrades to "no store"
|
|
257
|
+
// rather than throwing.
|
|
114
258
|
const credentialsProvider = ctx.get?.("credentials");
|
|
115
259
|
let credentialStore;
|
|
116
260
|
if (credentialsProvider) {
|
|
@@ -122,50 +266,181 @@ export async function apply(ctx, config = {}) {
|
|
|
122
266
|
}
|
|
123
267
|
}
|
|
124
268
|
const wanted = cfg.channels ?? CHANNELS;
|
|
269
|
+
// Migrate config-file secrets into the store before activation, so an upgraded
|
|
270
|
+
// user's already-working channels report as configured (and the store becomes
|
|
271
|
+
// the single source of truth). Best-effort: never blocks the plugin load.
|
|
272
|
+
if (credentialStore && credentialsProvider) {
|
|
273
|
+
try {
|
|
274
|
+
await migrateConfigSecrets(credentialsProvider, cfg, wanted);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Ignore a migration failure; activation continues with existing config.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
125
280
|
const finalCfg = credentialStore
|
|
126
281
|
? await injectSecrets(cfg, wanted, (name) => credentialStore.get(name))
|
|
127
282
|
: cfg;
|
|
128
283
|
// Each channel's register receives the connect instance directly.
|
|
129
284
|
const channels = {
|
|
130
|
-
|
|
285
|
+
// Feishu additionally gets the credential store so a completed one-click
|
|
286
|
+
// onboarding lands in the DSH store (and not only in its own legacy file).
|
|
287
|
+
feishu: (_ctx, channelConfig) => feishuRegister(connect, channelConfig, _ctx, { credentialStore }),
|
|
131
288
|
telegram: (_ctx, channelConfig) => telegramRegister(connect, channelConfig, _ctx),
|
|
132
289
|
dingtalk: (_ctx, channelConfig) => dingtalkRegister(connect, channelConfig, _ctx),
|
|
133
290
|
web: (_ctx, channelConfig) => webRegister(connect, channelConfig, _ctx),
|
|
134
291
|
};
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
292
|
+
// Channel lifecycle is owned by a runtime, not a one-shot activation: once
|
|
293
|
+
// the channel list is user-editable, dropping a channel has to *stop* its
|
|
294
|
+
// adapter (a live Feishu long connection would otherwise outlive its config)
|
|
295
|
+
// and a reconfigured channel has to restart before the old transport is
|
|
296
|
+
// still delivering. See `settings/channel-runtime.ts`.
|
|
297
|
+
const runtime = new ChannelRuntime({
|
|
298
|
+
ctx,
|
|
299
|
+
channels,
|
|
300
|
+
teardown: (name) => connect.unregisterAdapter(name),
|
|
301
|
+
getSecrets: credentialStore ? (name) => credentialStore.get(name) : undefined,
|
|
302
|
+
});
|
|
303
|
+
await runtime.apply(finalCfg);
|
|
304
|
+
// Web-settings pane state. The pane's store is the profile entry itself (see
|
|
305
|
+
// the namespace block below); this path backs only the *fallback* store, used
|
|
306
|
+
// by a host that has no settings service. (The one-shot legacy import used to
|
|
307
|
+
// record itself here too; it is anchored on the profile entry now, see
|
|
308
|
+
// `profilePatchPath` — this path is user-relocatable and deletable, which made
|
|
309
|
+
// it the wrong place to remember that a migration had run.) Resolved through
|
|
310
|
+
// the shared helper and off the *merged* config, so the state file lands beside
|
|
311
|
+
// `bindings.json` and can't disagree with the stores. This used to read the raw `cfg.stateDir`,
|
|
312
|
+
// which made the path `undefined` for every profile that never set
|
|
313
|
+
// `stateDir` — and an undefined path made the settings service degrade to
|
|
314
|
+
// in-memory, so a pane save silently vanished.
|
|
315
|
+
const settingsStatePath = finalCfg.settingsStatePath
|
|
316
|
+
?? join(resolveStateDir(finalCfg), "dsh-connect-settings.json");
|
|
317
|
+
// One projection, two triggers. A change reaches the adapters either through
|
|
318
|
+
// a write (the settings pane, or the legacy import) or through the loader
|
|
319
|
+
// committing new values into the volatile references behind `rawConfig`, and
|
|
320
|
+
// both must hand the runtime the *same* shape: `ChannelRuntime` restarts an
|
|
321
|
+
// adapter whose config it reads as different, so a mismatch here would bounce
|
|
322
|
+
// a live channel on every commit.
|
|
323
|
+
const reconcile = () => {
|
|
324
|
+
void runtime.apply(sectionOf(materializeConfig(rawConfig)));
|
|
152
325
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
326
|
+
// Register the `connect` profile entry's pane fields with the host, so the
|
|
327
|
+
// channel selection and the per-channel non-secret options become
|
|
328
|
+
// user-editable and *effective*: the pane writes them onto the entry, the
|
|
329
|
+
// loader commits them into the volatile references `rawConfig` carries, and
|
|
330
|
+
// every change re-reconciles the running adapters through `reconcile`. The
|
|
331
|
+
// panel's own RPC (`settings/settings-rpc.ts`) is a separate transport onto
|
|
332
|
+
// the same service and is kept for the pane's compatibility path.
|
|
333
|
+
//
|
|
334
|
+
// Deferred through `inject` for the same reason as the RPC below: `settings`
|
|
335
|
+
// is a `dsh-base` row loaded after a user plugin's `apply` runs, so a plain
|
|
336
|
+
// optional read would see `undefined` and never register anything. If the
|
|
337
|
+
// host never provides it the callback never runs and the plugin config
|
|
338
|
+
// stands on its own — which is exactly what `installConnectSection` falls
|
|
339
|
+
// back to.
|
|
340
|
+
//
|
|
341
|
+
// Set once the seam is live; read lazily by the settings service below, which
|
|
342
|
+
// is built before the deferred callback runs.
|
|
343
|
+
let liveSection;
|
|
344
|
+
const installNamespace = (scopeCtx) => {
|
|
345
|
+
const settings = scopeCtx.get?.("settings");
|
|
346
|
+
// Suppress the host's automatic Plugins-page form for this entry: this
|
|
347
|
+
// plugin ships its own client pane, and two generated editors of the same
|
|
348
|
+
// fields is how a save silently reverts. `owner` must be *our* fiber — the
|
|
349
|
+
// method's default is the settings service's own fiber, which would attach
|
|
350
|
+
// the policy to the service and leave this entry's page alone.
|
|
351
|
+
if (typeof settings?.configure === "function") {
|
|
352
|
+
try {
|
|
353
|
+
const release = settings.configure(CONNECT_PRESENTATION, ctx.fiber);
|
|
354
|
+
if (typeof release === "function")
|
|
355
|
+
ctx.effect(() => release);
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
scopeCtx.logger?.warn?.(`connect: could not suppress the automatic settings page (${errorText(error)})`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const installed = installConnectSection({
|
|
362
|
+
owner: scopeCtx,
|
|
363
|
+
settings,
|
|
364
|
+
config: rawConfig,
|
|
365
|
+
ns: entryId(ctx),
|
|
366
|
+
onChange: () => reconcile(),
|
|
367
|
+
});
|
|
368
|
+
liveSection = installed.handle;
|
|
369
|
+
// One-shot: fold a pre-0.2 `$DSH_HOME/settings.yaml` `dsh-connect:` section
|
|
370
|
+
// into the entry. The host's own legacy import renames that document and
|
|
371
|
+
// then drops this section — it has no entry by that name — so on an
|
|
372
|
+
// upgraded install this is the only remaining copy of the user's pane
|
|
373
|
+
// choices. Nothing happens when there is no such document, and `current` is
|
|
374
|
+
// the section in force, so the write cannot clear a field the legacy
|
|
375
|
+
// document does not mention.
|
|
376
|
+
const handle = installed.handle;
|
|
377
|
+
if (handle !== undefined) {
|
|
378
|
+
void importLegacySection({
|
|
379
|
+
home: harnessHome(scopeCtx),
|
|
380
|
+
write: (section) => handle.write(section),
|
|
381
|
+
current: installed.section,
|
|
382
|
+
statePath: settingsStatePath,
|
|
383
|
+
entryPath: profilePatchPath(scopeCtx),
|
|
384
|
+
logger: scopeCtx.logger,
|
|
385
|
+
});
|
|
159
386
|
}
|
|
160
|
-
|
|
161
|
-
|
|
387
|
+
};
|
|
388
|
+
if (typeof ctx.inject === "function") {
|
|
389
|
+
ctx.inject(["settings"], installNamespace);
|
|
162
390
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
391
|
+
else {
|
|
392
|
+
// Bare context (unit tests): no inject-scope API, install directly.
|
|
393
|
+
installNamespace(ctx);
|
|
394
|
+
}
|
|
395
|
+
// A pane save is not the only writer: editing a running profile's patch (or
|
|
396
|
+
// reloading the plugin entry) commits volatile paths without re-running
|
|
397
|
+
// `apply`. The host announces that here, so the adapters follow the config
|
|
398
|
+
// they were started from.
|
|
399
|
+
ctx.on("loader/volatile-update", () => reconcile());
|
|
400
|
+
// Expose the web-settings RPC. The host `connection`/`webServer` services are
|
|
401
|
+
// loaded as base plugins AFTER this user plugin's apply runs, so a plain
|
|
402
|
+
// optional inject would see `ctx.connection === undefined` and mount nothing
|
|
403
|
+
// (the settings pane then hangs on "加载中"). Instead defer the registration
|
|
404
|
+
// into a nested injection scope that waits for both to be ready — the same
|
|
405
|
+
// pattern dsh-api-gateway uses for its `/api` route. If the host never
|
|
406
|
+
// provides them (non-web runtime), the callback never runs and the RPC is
|
|
407
|
+
// simply absent.
|
|
408
|
+
//
|
|
409
|
+
// Both services are required in the injected set: `installSettingsRpc` mounts
|
|
410
|
+
// the channel as a `prefix` route on `webServer` itself (see its doc for why
|
|
411
|
+
// `connection.rpc.handle` cannot be used for a third-party channel), and it
|
|
412
|
+
// reads `connection.requestRejection` for the Host/Origin + browser-auth
|
|
413
|
+
// fence.
|
|
414
|
+
//
|
|
415
|
+
// Web settings pane state. When the namespace above is live, that registration
|
|
416
|
+
// *is* the store: the pane reads and writes this plugin's entry in the active
|
|
417
|
+
// profile patch (`cordis.patch.yml`) through `liveSection`, a save reconciles
|
|
418
|
+
// the adapters immediately (via the same `onChange`) and survives a restart.
|
|
419
|
+
// The JSON file below is then only a fallback for a host with no settings
|
|
420
|
+
// service, which is why the service takes the handle as a getter, not a value.
|
|
421
|
+
// Credentials are a second call, and a secret is not config: `onCredentialsSaved`
|
|
422
|
+
// is the reconcile trigger for that plane, since no config write happens.
|
|
423
|
+
//
|
|
424
|
+
// Seeded from the live plugin config so the pane reflects the channels
|
|
425
|
+
// actually enabled; only NON-SECRET *declared* fields (secrets live in the
|
|
426
|
+
// credential store, never in either store this file touches), which is
|
|
427
|
+
// exactly the projection `sectionOf` performs — the same one the write path
|
|
428
|
+
// applies, so the fallback store cannot drift from the entry.
|
|
429
|
+
const settingsSeed = { ...sectionOf(finalCfg) };
|
|
430
|
+
const settingsService = createSettingsService({
|
|
431
|
+
statePath: settingsStatePath,
|
|
432
|
+
credentialStore,
|
|
433
|
+
initialConfig: settingsSeed,
|
|
434
|
+
live: () => liveSection,
|
|
435
|
+
// A rotated secret is inert until the adapters are re-applied, and the
|
|
436
|
+
// namespace's `onChange` only fires for a config write — the pane saves
|
|
437
|
+
// credentials as a second, separate call. Without this the user pastes a
|
|
438
|
+
// new appSecret, is told 「已保存」, and the bot stays broken until restart.
|
|
439
|
+
onCredentialsSaved: () => reconcile(),
|
|
440
|
+
});
|
|
166
441
|
if (typeof ctx.inject === "function") {
|
|
167
|
-
ctx.inject(["connection"], (
|
|
168
|
-
installSettingsRpc(
|
|
442
|
+
ctx.inject(["connection", "webServer"], (scopeCtx) => {
|
|
443
|
+
installSettingsRpc(scopeCtx, { service: settingsService });
|
|
169
444
|
});
|
|
170
445
|
}
|
|
171
446
|
else {
|