@xmanrui/dsh-im 4.3.0 → 4.5.0
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.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +976 -634
- package/lib/index.js +241 -238
- package/package.json +1 -1
- package/plugin-src/client/access-policy-settings.js +351 -0
- package/plugin-src/client/channel-card-meta.js +2 -1
- package/plugin-src/client/channels/dingtalk/api.js +5 -0
- package/plugin-src/client/channels/dingtalk/index.js +1 -0
- package/plugin-src/client/channels/feishu/api.js +5 -0
- package/plugin-src/client/channels/feishu/index.js +1 -0
- package/plugin-src/client/channels/qq/api.js +5 -0
- package/plugin-src/client/channels/qq/index.js +1 -0
- package/plugin-src/client/channels/shared/token-api.js +5 -0
- package/plugin-src/client/channels/shared/token-channel.js +1 -0
- package/plugin-src/client/channels/telegram/api.js +2 -17
- package/plugin-src/client/channels/telegram/index.js +0 -112
- package/plugin-src/client/channels/telegram/styles.js +0 -28
- package/plugin-src/client/channels/wecom/api.js +5 -0
- package/plugin-src/client/channels/wecom/index.js +1 -0
- package/plugin-src/client/channels/weixin/api.js +5 -0
- package/plugin-src/client/channels/weixin/index.js +1 -0
- package/plugin-src/client/channels/whatsapp/api.js +4 -10
- package/plugin-src/client/channels/whatsapp/index.js +1 -125
- package/plugin-src/client/channels/whatsapp/styles.js +0 -25
- package/plugin-src/client/context-enhancement.js +19 -5
- package/plugin-src/client/delivery-settings.js +61 -9
- package/plugin-src/client/i18n.js +61 -2
- package/plugin-src/client/index.js +1 -0
- package/plugin-src/client/styles.js +50 -4
- package/plugin-src/host/channels/dingtalk/production.mjs +12 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +39 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +19 -2
- package/plugin-src/host/channels/qq/production.mjs +12 -1
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +137 -0
- package/plugin-src/host/channels/shared/access-policy-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +15 -1
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/production.mjs +12 -1
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/rpc.mjs +2 -49
- package/plugin-src/host/channels/wecom/production.mjs +12 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/production.mjs +12 -1
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +16 -2
- package/plugin-src/host/channels/whatsapp/rpc.mjs +10 -15
- package/plugin-src/host/modern-harness-api.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-bridge.mjs +70 -12
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -0
- package/src/channels/discord/discord-runtime.mjs +29 -1
- package/src/channels/feishu/bridge.mjs +207 -82
- package/src/channels/feishu/feishu-cards.mjs +2 -2
- package/src/channels/feishu/feishu-channel.mjs +36 -6
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +67 -20
- package/src/channels/qq/qq-runtime.mjs +20 -2
- package/src/channels/shared/access-policy.mjs +210 -0
- package/src/channels/shared/bot-workspace-store.mjs +181 -33
- package/src/channels/shared/command-permission.mjs +32 -0
- package/src/channels/shared/compact-command.mjs +5 -2
- package/src/channels/shared/context-enhancement.mjs +9 -4
- package/src/channels/shared/harness-client.mjs +6 -0
- package/src/channels/shared/i18n-en/feishu.mjs +3 -3
- package/src/channels/shared/i18n-en/shared-a.mjs +2 -1
- package/src/channels/shared/i18n-en/shared-b.mjs +6 -3
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -0
- package/src/channels/shared/inbound-access.mjs +43 -0
- package/src/channels/shared/session-title.mjs +74 -0
- package/src/channels/shared/text-harness-bridge.mjs +54 -6
- package/src/channels/shared/workspace-command.mjs +26 -5
- package/src/channels/shared/workspace-session.mjs +28 -2
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +6 -13
- package/src/channels/wecom/wecom-bridge.mjs +57 -6
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +67 -19
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +42 -20
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
normalizeAgentPresetCatalog,
|
|
14
14
|
validateAgentPresetId,
|
|
15
15
|
} from './agent-preset.mjs';
|
|
16
|
+
import {
|
|
17
|
+
normalizeAccessPolicy,
|
|
18
|
+
validateAccessPolicy,
|
|
19
|
+
} from './access-policy.mjs';
|
|
16
20
|
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
|
17
21
|
import {
|
|
18
22
|
DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
|
|
@@ -131,6 +135,26 @@ function normalizeDeliveryTargets(value) {
|
|
|
131
135
|
return deliveryTargets;
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
function normalizeAccessPolicies(value, workspaces) {
|
|
139
|
+
const accessPolicies = Object.create(null);
|
|
140
|
+
if (value === undefined) return accessPolicies;
|
|
141
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
142
|
+
// Preserve the distinction between a missing policy (eligible for startup
|
|
143
|
+
// initialization) and damaged persisted data (fail closed).
|
|
144
|
+
for (const botId of Object.keys(workspaces)) accessPolicies[botId] = null;
|
|
145
|
+
return accessPolicies;
|
|
146
|
+
}
|
|
147
|
+
for (const [botId, policy] of Object.entries(value)) {
|
|
148
|
+
try {
|
|
149
|
+
botIdOf(botId);
|
|
150
|
+
accessPolicies[botId] = normalizeAccessPolicy(policy);
|
|
151
|
+
} catch {
|
|
152
|
+
// An invalid key cannot identify a bot, so it is isolated and ignored.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return accessPolicies;
|
|
156
|
+
}
|
|
157
|
+
|
|
134
158
|
function normalizeDocument(value) {
|
|
135
159
|
if (!value || ![1, 2].includes(value.version) || !value.workspaces
|
|
136
160
|
|| typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return null;
|
|
@@ -168,15 +192,52 @@ function normalizeDocument(value) {
|
|
|
168
192
|
if (value.version === 1 && value.deliveryTargets !== undefined) return null;
|
|
169
193
|
const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets);
|
|
170
194
|
if (!deliveryTargets) return null;
|
|
195
|
+
const accessPolicies = normalizeAccessPolicies(value.accessPolicies, workspaces);
|
|
196
|
+
const version = value.accessPolicies === undefined ? value.version : 2;
|
|
171
197
|
return {
|
|
172
|
-
|
|
198
|
+
// A v1 file cannot be emitted with this optional v2 section. If one is
|
|
199
|
+
// recovered from an interrupted/manual edit, retain it on the next write.
|
|
200
|
+
version,
|
|
173
201
|
workspaces,
|
|
174
202
|
agentPresets,
|
|
175
203
|
contextEnhancement,
|
|
176
204
|
deliveryTargets,
|
|
205
|
+
accessPolicies,
|
|
177
206
|
};
|
|
178
207
|
}
|
|
179
208
|
|
|
209
|
+
function storedDocument({
|
|
210
|
+
version,
|
|
211
|
+
workspaces,
|
|
212
|
+
agentPresets,
|
|
213
|
+
contextEnhancement,
|
|
214
|
+
deliveryTargets,
|
|
215
|
+
accessPolicies,
|
|
216
|
+
}) {
|
|
217
|
+
const document = { version, workspaces };
|
|
218
|
+
if (Object.keys(agentPresets).length > 0) document.agentPresets = agentPresets;
|
|
219
|
+
if (Object.keys(contextEnhancement).length > 0) {
|
|
220
|
+
document.contextEnhancement = contextEnhancement;
|
|
221
|
+
}
|
|
222
|
+
if (version >= 2 && Object.keys(deliveryTargets).length > 0) {
|
|
223
|
+
document.deliveryTargets = deliveryTargets;
|
|
224
|
+
}
|
|
225
|
+
if (version >= 2 && Object.keys(accessPolicies).length > 0) {
|
|
226
|
+
document.accessPolicies = accessPolicies;
|
|
227
|
+
}
|
|
228
|
+
return document;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeStoredDocument(path, document) {
|
|
232
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
233
|
+
const temporary = `${path}.tmp`;
|
|
234
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
235
|
+
encoding: 'utf8',
|
|
236
|
+
mode: 0o600,
|
|
237
|
+
});
|
|
238
|
+
await rename(temporary, path);
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
export async function validateWorkspacePath(value) {
|
|
181
242
|
if (typeof value !== 'string' || !value.trim() || !isAbsolute(value.trim())) {
|
|
182
243
|
const error = new Error('工作区必须是绝对路径。');
|
|
@@ -208,6 +269,7 @@ export class BotWorkspaceStore {
|
|
|
208
269
|
#agentPresets = {};
|
|
209
270
|
#contextEnhancement = {};
|
|
210
271
|
#deliveryTargets = Object.create(null);
|
|
272
|
+
#accessPolicies = Object.create(null);
|
|
211
273
|
#generations = new Map();
|
|
212
274
|
#nextGeneration = 1;
|
|
213
275
|
#incarnations = new Map();
|
|
@@ -233,6 +295,7 @@ export class BotWorkspaceStore {
|
|
|
233
295
|
this.#agentPresets = normalized.agentPresets;
|
|
234
296
|
this.#contextEnhancement = normalized.contextEnhancement;
|
|
235
297
|
this.#deliveryTargets = normalized.deliveryTargets;
|
|
298
|
+
this.#accessPolicies = normalized.accessPolicies;
|
|
236
299
|
} catch (error) {
|
|
237
300
|
if (error?.code !== 'ENOENT') throw error;
|
|
238
301
|
this.#version = 1;
|
|
@@ -240,6 +303,7 @@ export class BotWorkspaceStore {
|
|
|
240
303
|
this.#agentPresets = {};
|
|
241
304
|
this.#contextEnhancement = {};
|
|
242
305
|
this.#deliveryTargets = Object.create(null);
|
|
306
|
+
this.#accessPolicies = Object.create(null);
|
|
243
307
|
}
|
|
244
308
|
this.#generations.clear();
|
|
245
309
|
this.#nextGeneration = 1;
|
|
@@ -278,6 +342,13 @@ export class BotWorkspaceStore {
|
|
|
278
342
|
: DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
|
|
279
343
|
}
|
|
280
344
|
|
|
345
|
+
accessPolicyFor(botId) {
|
|
346
|
+
const id = botIdOf(botId);
|
|
347
|
+
return this.has(id) && Object.hasOwn(this.#accessPolicies, id)
|
|
348
|
+
? this.#accessPolicies[id]
|
|
349
|
+
: null;
|
|
350
|
+
}
|
|
351
|
+
|
|
281
352
|
listDeliveryTargets(botId) {
|
|
282
353
|
const id = botIdOf(botId);
|
|
283
354
|
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
|
@@ -373,28 +444,53 @@ export class BotWorkspaceStore {
|
|
|
373
444
|
}
|
|
374
445
|
}
|
|
375
446
|
|
|
376
|
-
async ensure(botId, {
|
|
447
|
+
async ensure(botId, {
|
|
448
|
+
workspace = this.#defaultWorkspace,
|
|
449
|
+
defaultAgentPreset,
|
|
450
|
+
initialAccessPolicy,
|
|
451
|
+
} = {}) {
|
|
377
452
|
const id = botIdOf(botId);
|
|
378
453
|
const initialWorkspace = resolve(workspace);
|
|
379
454
|
return this.#enqueue(id, async () => {
|
|
380
|
-
|
|
381
|
-
|
|
455
|
+
const createsBot = !this.#workspaces[id];
|
|
456
|
+
const initializesAccessPolicy = initialAccessPolicy !== undefined
|
|
457
|
+
&& !Object.hasOwn(this.#accessPolicies, id);
|
|
458
|
+
if (createsBot || initializesAccessPolicy) {
|
|
459
|
+
const accessPolicy = initializesAccessPolicy
|
|
460
|
+
? validateAccessPolicy(initialAccessPolicy)
|
|
461
|
+
: undefined;
|
|
462
|
+
const agentPreset = createsBot ? validateAgentPresetId(defaultAgentPreset) : null;
|
|
382
463
|
const hadAgentPreset = Object.hasOwn(this.#agentPresets, id);
|
|
383
464
|
const previousAgentPreset = this.#agentPresets[id];
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
465
|
+
const nextAccessPolicies = initializesAccessPolicy
|
|
466
|
+
? { ...this.#accessPolicies, [id]: accessPolicy }
|
|
467
|
+
: this.#accessPolicies;
|
|
468
|
+
if (createsBot) {
|
|
469
|
+
this.#workspaces[id] = initialWorkspace;
|
|
470
|
+
if (agentPreset) this.#agentPresets[id] = agentPreset;
|
|
471
|
+
this.#generations.set(id, this.#freshGeneration());
|
|
472
|
+
this.#incarnations.set(id, this.#freshIncarnation());
|
|
473
|
+
}
|
|
474
|
+
const nextVersion = initializesAccessPolicy ? 2 : this.#version;
|
|
388
475
|
try {
|
|
389
|
-
await this.#persist(
|
|
476
|
+
await this.#persist(
|
|
477
|
+
this.#contextEnhancement,
|
|
478
|
+
this.#deliveryTargets,
|
|
479
|
+
nextVersion,
|
|
480
|
+
nextAccessPolicies,
|
|
481
|
+
);
|
|
390
482
|
} catch (error) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
483
|
+
if (createsBot) {
|
|
484
|
+
delete this.#workspaces[id];
|
|
485
|
+
if (hadAgentPreset) this.#agentPresets[id] = previousAgentPreset;
|
|
486
|
+
else delete this.#agentPresets[id];
|
|
487
|
+
this.#generations.delete(id);
|
|
488
|
+
this.#incarnations.delete(id);
|
|
489
|
+
}
|
|
396
490
|
throw error;
|
|
397
491
|
}
|
|
492
|
+
this.#accessPolicies = nextAccessPolicies;
|
|
493
|
+
this.#version = nextVersion;
|
|
398
494
|
} else if (!this.#generations.has(id)) {
|
|
399
495
|
this.#generations.set(id, this.#freshGeneration());
|
|
400
496
|
}
|
|
@@ -487,6 +583,30 @@ export class BotWorkspaceStore {
|
|
|
487
583
|
});
|
|
488
584
|
}
|
|
489
585
|
|
|
586
|
+
async setAccessPolicy(botId, value, { incarnation } = {}) {
|
|
587
|
+
const id = botIdOf(botId);
|
|
588
|
+
const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
|
|
589
|
+
const policy = validateAccessPolicy(value);
|
|
590
|
+
return this.#enqueue(id, async () => {
|
|
591
|
+
if (!this.has(id) || expectedIncarnation !== this.incarnationFor(id)) {
|
|
592
|
+
const error = new Error('找不到要修改的机器人。');
|
|
593
|
+
error.code = 'workspace-bot-not-found';
|
|
594
|
+
throw error;
|
|
595
|
+
}
|
|
596
|
+
const next = { ...this.#accessPolicies, [id]: policy };
|
|
597
|
+
// Inbound messages keep the previous committed snapshot until rename succeeds.
|
|
598
|
+
await this.#persist(
|
|
599
|
+
this.#contextEnhancement,
|
|
600
|
+
this.#deliveryTargets,
|
|
601
|
+
2,
|
|
602
|
+
next,
|
|
603
|
+
);
|
|
604
|
+
this.#accessPolicies = next;
|
|
605
|
+
this.#version = 2;
|
|
606
|
+
return policy;
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
490
610
|
async bindWorkspaceSession(botId, value, {
|
|
491
611
|
conversationKey,
|
|
492
612
|
sessionId,
|
|
@@ -646,6 +766,7 @@ export class BotWorkspaceStore {
|
|
|
646
766
|
...Object.keys(this.#agentPresets),
|
|
647
767
|
...Object.keys(this.#contextEnhancement),
|
|
648
768
|
...Object.keys(this.#deliveryTargets),
|
|
769
|
+
...Object.keys(this.#accessPolicies),
|
|
649
770
|
...this.#dirtyRemovals,
|
|
650
771
|
]);
|
|
651
772
|
for (const botId of candidates) {
|
|
@@ -663,6 +784,7 @@ export class BotWorkspaceStore {
|
|
|
663
784
|
workspace: this.workspaceFor(bot.botId),
|
|
664
785
|
agentPreset: this.agentPresetFor(bot.botId),
|
|
665
786
|
contextEnhancement: this.contextEnhancementFor(bot.botId),
|
|
787
|
+
accessPolicy: this.accessPolicyFor(bot.botId),
|
|
666
788
|
}
|
|
667
789
|
: bot),
|
|
668
790
|
};
|
|
@@ -694,12 +816,14 @@ export class BotWorkspaceStore {
|
|
|
694
816
|
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
|
695
817
|
const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
|
|
696
818
|
const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
|
|
819
|
+
const hadAccessPolicy = Object.hasOwn(this.#accessPolicies, id);
|
|
697
820
|
const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
|
|
698
|
-
|| hadDeliveryTargets || this.#dirtyRemovals.has(id);
|
|
821
|
+
|| hadDeliveryTargets || hadAccessPolicy || this.#dirtyRemovals.has(id);
|
|
699
822
|
delete this.#workspaces[id];
|
|
700
823
|
delete this.#agentPresets[id];
|
|
701
824
|
delete this.#contextEnhancement[id];
|
|
702
825
|
delete this.#deliveryTargets[id];
|
|
826
|
+
delete this.#accessPolicies[id];
|
|
703
827
|
this.#generations.delete(id);
|
|
704
828
|
this.#incarnations.delete(id);
|
|
705
829
|
if (!needsCleanup) return {
|
|
@@ -733,24 +857,16 @@ export class BotWorkspaceStore {
|
|
|
733
857
|
contextEnhancement = this.#contextEnhancement,
|
|
734
858
|
deliveryTargets = this.#deliveryTargets,
|
|
735
859
|
version = this.#version,
|
|
860
|
+
accessPolicies = this.#accessPolicies,
|
|
736
861
|
) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
document.deliveryTargets = deliveryTargets;
|
|
746
|
-
}
|
|
747
|
-
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
748
|
-
const temporary = `${this.#path}.tmp`;
|
|
749
|
-
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
750
|
-
encoding: 'utf8',
|
|
751
|
-
mode: 0o600,
|
|
752
|
-
});
|
|
753
|
-
await rename(temporary, this.#path);
|
|
862
|
+
await writeStoredDocument(this.#path, storedDocument({
|
|
863
|
+
version,
|
|
864
|
+
workspaces: this.#workspaces,
|
|
865
|
+
agentPresets: this.#agentPresets,
|
|
866
|
+
contextEnhancement,
|
|
867
|
+
deliveryTargets,
|
|
868
|
+
accessPolicies,
|
|
869
|
+
}));
|
|
754
870
|
this.#dirtyRemovals.clear();
|
|
755
871
|
}
|
|
756
872
|
|
|
@@ -758,7 +874,8 @@ export class BotWorkspaceStore {
|
|
|
758
874
|
if (Object.keys(this.#workspaces).length > 0
|
|
759
875
|
|| Object.keys(this.#agentPresets).length > 0
|
|
760
876
|
|| Object.keys(this.#contextEnhancement).length > 0
|
|
761
|
-
|| Object.keys(this.#deliveryTargets).length > 0
|
|
877
|
+
|| Object.keys(this.#deliveryTargets).length > 0
|
|
878
|
+
|| Object.keys(this.#accessPolicies).length > 0) {
|
|
762
879
|
await this.#persist();
|
|
763
880
|
return;
|
|
764
881
|
}
|
|
@@ -1094,6 +1211,11 @@ export function createBotWorkspaceScope(
|
|
|
1094
1211
|
readHistory(...args) {
|
|
1095
1212
|
return invokeCurrentSession('readSessionHistory', args, 'history read');
|
|
1096
1213
|
},
|
|
1214
|
+
...(typeof target.renameSession === 'function' ? {
|
|
1215
|
+
renameTitle(...args) {
|
|
1216
|
+
return invokeStartedSessionMutation('renameSession', args, 'title rename');
|
|
1217
|
+
},
|
|
1218
|
+
} : {}),
|
|
1097
1219
|
selectModel(...args) {
|
|
1098
1220
|
return invokeCurrentSession('selectSessionModel', args, 'model selection');
|
|
1099
1221
|
},
|
|
@@ -1280,6 +1402,31 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|
|
1280
1402
|
return result;
|
|
1281
1403
|
});
|
|
1282
1404
|
};
|
|
1405
|
+
const updateAccessPolicy = (botId, value, projectStatus) => {
|
|
1406
|
+
const incarnation = workspaces.incarnationFor(botId);
|
|
1407
|
+
const policy = validateAccessPolicy(value);
|
|
1408
|
+
return withBotTransition(botId, async () => {
|
|
1409
|
+
const snapshot = await controller.status();
|
|
1410
|
+
if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
|
|
1411
|
+
const error = new Error('找不到要修改的机器人。');
|
|
1412
|
+
error.code = 'workspace-bot-not-found';
|
|
1413
|
+
throw error;
|
|
1414
|
+
}
|
|
1415
|
+
const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
|
|
1416
|
+
const decorated = workspaces.decorateStatus(snapshot);
|
|
1417
|
+
const updated = {
|
|
1418
|
+
...decorated,
|
|
1419
|
+
bots: decorated.bots.map((bot) => bot?.botId === botId
|
|
1420
|
+
? { ...bot, accessPolicy: policy } : bot),
|
|
1421
|
+
...(catalog ? { agentPresetCatalog: catalog } : {}),
|
|
1422
|
+
};
|
|
1423
|
+
// Prepare the complete channel-specific response before commit. Failed
|
|
1424
|
+
// projections and disk writes must leave the live policy unchanged.
|
|
1425
|
+
const result = projectStatus ? await projectStatus(updated) : updated;
|
|
1426
|
+
await workspaces.setAccessPolicy(botId, policy, { incarnation });
|
|
1427
|
+
return result;
|
|
1428
|
+
});
|
|
1429
|
+
};
|
|
1283
1430
|
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
|
1284
1431
|
// Fence the old runtime without changing the durable mapping. A crash
|
|
1285
1432
|
// before the controller removes its config therefore keeps the bot's
|
|
@@ -1320,6 +1467,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|
|
1320
1467
|
if (property === 'updateWorkspace') return updateWorkspace;
|
|
1321
1468
|
if (property === 'updateAgentPreset') return updateAgentPreset;
|
|
1322
1469
|
if (property === 'updateContextEnhancement') return updateContextEnhancement;
|
|
1470
|
+
if (property === 'updateAccessPolicy') return updateAccessPolicy;
|
|
1323
1471
|
const value = Reflect.get(target, property, target);
|
|
1324
1472
|
if (typeof value !== 'function') return value;
|
|
1325
1473
|
if (property === 'deleteBot') {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isBatchInputCommand } from './batch-input.mjs';
|
|
2
|
+
import { isCompactCommand } from './compact-command.mjs';
|
|
3
|
+
import { isControlCommand } from './control-command.mjs';
|
|
4
|
+
import { isHistoryCommand } from './history-command.mjs';
|
|
5
|
+
import { isModelCommand } from './model-command.mjs';
|
|
6
|
+
import { isPresetCommand } from './preset-command.mjs';
|
|
7
|
+
import { isWorkspaceCommand } from './workspace-command.mjs';
|
|
8
|
+
|
|
9
|
+
const SIMPLE_TEXT_COMMANDS = new Set(['/help', '/status', '/new']);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Match only commands that the shared bridges already execute locally.
|
|
13
|
+
* Unknown slash-prefixed text remains an ordinary prompt.
|
|
14
|
+
*/
|
|
15
|
+
export function isSharedLocalCommand(text, {
|
|
16
|
+
hasImages = false,
|
|
17
|
+
hasFiles = false,
|
|
18
|
+
} = {}) {
|
|
19
|
+
if (typeof text !== 'string') return false;
|
|
20
|
+
const command = text.trim();
|
|
21
|
+
if (!command) return false;
|
|
22
|
+
if (isBatchInputCommand(command) || isHistoryCommand(command)) return true;
|
|
23
|
+
if (!hasFiles && (
|
|
24
|
+
isControlCommand(command)
|
|
25
|
+
|| isModelCommand(command)
|
|
26
|
+
|| isPresetCommand(command)
|
|
27
|
+
)) return true;
|
|
28
|
+
if (hasImages || hasFiles) return false;
|
|
29
|
+
return SIMPLE_TEXT_COMMANDS.has(command.toLowerCase())
|
|
30
|
+
|| isWorkspaceCommand(command)
|
|
31
|
+
|| isCompactCommand(command);
|
|
32
|
+
}
|
|
@@ -70,9 +70,8 @@ function compactErrorMessage(error) {
|
|
|
70
70
|
* Unknown input returns null so the caller may continue ordinary message routing.
|
|
71
71
|
*/
|
|
72
72
|
export async function runCompactCommand(text, harness, state, conversationKey, options = {}) {
|
|
73
|
-
if (
|
|
73
|
+
if (!isCompactCommand(text)) return null;
|
|
74
74
|
const match = COMPACT_COMMAND.exec(text.trim());
|
|
75
|
-
if (!match) return null;
|
|
76
75
|
if (match[1].trim()) return commandResult(t(COMPACT_USAGE));
|
|
77
76
|
if (typeof state?.sessionFor !== 'function') {
|
|
78
77
|
return commandResult(t('当前机器人没有可用的会话状态。'));
|
|
@@ -94,3 +93,7 @@ export async function runCompactCommand(text, harness, state, conversationKey, o
|
|
|
94
93
|
return commandResult(compactErrorMessage(error));
|
|
95
94
|
}
|
|
96
95
|
}
|
|
96
|
+
|
|
97
|
+
export function isCompactCommand(text) {
|
|
98
|
+
return typeof text === 'string' && COMPACT_COMMAND.test(text.trim());
|
|
99
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Shared by the Host and settings UI; keep this module browser-compatible.
|
|
2
2
|
export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
|
|
3
|
-
'channel', 'conversationType', 'senderId', 'senderName', 'botId',
|
|
3
|
+
'channel', 'conversationType', 'senderId', 'senderName', 'conversationTitle', 'botId',
|
|
4
4
|
]);
|
|
5
5
|
|
|
6
6
|
export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
|
|
@@ -36,7 +36,10 @@ const CHANNELS = new Set([
|
|
|
36
36
|
'wecom', 'weixin', 'feishu', 'dingtalk', 'qq',
|
|
37
37
|
'slack', 'telegram', 'discord', 'whatsapp',
|
|
38
38
|
]);
|
|
39
|
-
const SOURCE_LIMITS = {
|
|
39
|
+
const SOURCE_LIMITS = {
|
|
40
|
+
channel: 16, conversationType: 6, senderId: 256, senderName: 256,
|
|
41
|
+
conversationTitle: 256, botId: 128,
|
|
42
|
+
};
|
|
40
43
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
|
41
44
|
|
|
42
45
|
function invalidConfig(message) {
|
|
@@ -61,7 +64,7 @@ function validateContextEnhancementScope(input) {
|
|
|
61
64
|
throw invalidConfig('群聊和私聊开关必须是布尔值。');
|
|
62
65
|
}
|
|
63
66
|
if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
|
|
64
|
-
throw invalidConfig('
|
|
67
|
+
throw invalidConfig('来源字段只能选择已定义的六个字段。');
|
|
65
68
|
}
|
|
66
69
|
if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
|
|
67
70
|
throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
|
|
@@ -146,7 +149,9 @@ function sourceString(value, field) {
|
|
|
146
149
|
|
|
147
150
|
function sourceBlock(snapshot, sourceFactory) {
|
|
148
151
|
const { fields } = snapshot.config;
|
|
149
|
-
const needsSource = fields.some((field) => [
|
|
152
|
+
const needsSource = fields.some((field) => [
|
|
153
|
+
'channel', 'senderId', 'senderName', 'conversationTitle',
|
|
154
|
+
].includes(field));
|
|
150
155
|
const source = needsSource ? sourceFactory?.() : null;
|
|
151
156
|
const projected = {};
|
|
152
157
|
for (const field of fields) {
|
|
@@ -911,6 +911,12 @@ export class HarnessClient {
|
|
|
911
911
|
return created.sessionId;
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
+
async renameSession(sessionId, title, options = {}) {
|
|
915
|
+
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
916
|
+
if (typeof title !== 'string' || !title.trim()) throw new TypeError('session title is required');
|
|
917
|
+
return this.rpc('session.rename', { sessionId, title }, 30_000, options);
|
|
918
|
+
}
|
|
919
|
+
|
|
914
920
|
async executeCommand(sessionId, line, options = {}) {
|
|
915
921
|
if (typeof sessionId !== 'string' || !sessionId) throw new TypeError('sessionId is required');
|
|
916
922
|
if (typeof line !== 'string' || !line) throw new TypeError('command line is required');
|
|
@@ -227,7 +227,6 @@ export default {
|
|
|
227
227
|
'/sessionlist or /sessions List workspace sessions',
|
|
228
228
|
'/session ID 绑定已有会话': '/session ID Bind an existing session',
|
|
229
229
|
'/workspacelist 列出工作区': '/workspacelist List workspaces',
|
|
230
|
-
'/workspace 路径 切换工作区': '/workspace PATH Switch workspace',
|
|
231
230
|
'/new 开启全新会话': '/new Start a new session',
|
|
232
231
|
'📊 状态 / 压缩': '📊 Status / compact',
|
|
233
232
|
'/status 连接状态': '/status Connection status',
|
|
@@ -247,8 +246,8 @@ export default {
|
|
|
247
246
|
'/steer 指令 给 Agent 补充指令': '/steer INSTRUCTION Steer the Agent',
|
|
248
247
|
'**📋 卡片功能**\n\n1. 会话下拉 — 切换当前绑定会话\n2. 工作区下拉 — 切换工作区\n3. 🤖 预设下拉 — 切换 Agent 预设\n4. 🧠 模型下拉 — 切换模型\n5. 🆕 新会话 — 开启全新会话\n6. 📋 会话/关注 — 查看/绑定会话,管理关注\n7. ⏹ 停止 — 停止当前任务\n8. 📐 压缩 — 压缩当前会话上下文\n9. 补充指令 — 给 Agent 发送指令\n10. 🗄 归档切换 — 显示/隐藏归档会话\n11. 📊 状态 — 查看系统连接状态\n12. 📖 帮助 — 查看本帮助':
|
|
249
248
|
'**📋 Card features**\n\n1. Session dropdown — switch the bound session\n2. Workspace dropdown — switch workspace\n3. 🤖 Preset dropdown — switch Agent Preset\n4. 🧠 Model dropdown — switch model\n5. 🆕 New session — start fresh\n6. 📋 Sessions/watches — view or bind sessions and manage watches\n7. ⏹ Stop — stop the current task\n8. 📐 Compact — compact the current session context\n9. Steer task — send an instruction to the Agent\n10. 🗄 Archived toggle — show or hide archived sessions\n11. 📊 Status — view connection status\n12. 📖 Help — view this help',
|
|
250
|
-
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/workspace
|
|
251
|
-
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/workspace
|
|
249
|
+
'**⌨️ 文本命令**\n\n`/m` — 打开菜单卡片\n`/new` — 开启全新会话\n`/session ID` — 绑定已有会话\n`/sessionlist [工作区]` 或 `/sessions [工作区]` — 列出会话\n`/workspace 工作区序号或绝对路径` — 切换工作区\n`/workspacelist` — 列出工作区\n`/status` — 查看连接状态\n`/compact` — 压缩上下文\n`/stop` — 停止当前任务\n`/steer 指令` — 补充指令\n`/watch ID` — 关注会话\n`/watchlist` — 关注列表\n`/unwatch ID` — 取消关注\n`/archived on/off` — 归档显隐\n`/presetlist` 或 `/presets` — 列出预设\n`/preset [序号/ID]` — 切换预设\n`/preset --default` — 跟随默认\n`/models` — 列出模型\n`/reasoninglist` 或 `/reasonings` — 按序号列出当前模型可用推理等级\n`/reasoning [序号、等级ID或 --default]` — 查看或切换当前推理等级\n`/model [序号或完整模型ID] [推理等级ID]` — 查看或切换当前会话模型\n`/batch` — 开启批量输入(仅私聊,最多 10 条文字)\n`/send` — 提交当前批次\n`/cancel` — 取消当前批次\n`/repair` — 补全飞书权限与卡片回调':
|
|
250
|
+
'**⌨️ Text commands**\n\n`/m` — open the menu card\n`/new` — start a new session\n`/session ID` — bind an existing session\n`/sessionlist [workspace]` or `/sessions [workspace]` — list sessions\n`/workspace <workspace index or absolute path>` — switch workspace\n`/workspacelist` — list workspaces\n`/status` — view connection status\n`/compact` — compact context\n`/stop` — stop the current task\n`/steer INSTRUCTION` — steer the task\n`/watch ID` — watch a session\n`/watchlist` — list watched sessions\n`/unwatch ID` — stop watching\n`/archived on/off` — show or hide archived sessions\n`/presetlist` or `/presets` — list presets\n`/preset [index/ID]` — switch preset\n`/preset --default` — follow default\n`/models` — list models\n`/reasoninglist` or `/reasonings` — list reasoning efforts for the current model\n`/reasoning [index, effort ID, or --default]` — show or switch reasoning effort\n`/model [index or full model ID] [reasoning effort ID]` — show or switch the current Session model\n`/batch` — start batch input (direct messages only, up to 10 text messages)\n`/send` — submit the current batch\n`/cancel` — cancel the current batch\n`/repair` — complete Feishu permissions and the card callback',
|
|
252
251
|
'**💡 数字兜底**\n回复数字快速操作:\n**1**工作区列表 · **2**新会话 · **3**会话/关注\n**4**状态 · **5**补全权限 · **6**帮助':
|
|
253
252
|
'**💡 Number fallback**\nReply with a number for a quick action:\n**1** Workspace list · **2** New session · **3** Sessions/watches\n**4** Status · **5** Complete permissions · **6** Help',
|
|
254
253
|
'从下方下拉选择补充指令;最后一项可自定义输入。':
|
|
@@ -338,6 +337,7 @@ export default {
|
|
|
338
337
|
|
|
339
338
|
// feishu/feishu-channel.mjs
|
|
340
339
|
'正在生成…': 'Generating…',
|
|
340
|
+
'⤵️ 最终结果见下方': '⤵️ Final result below',
|
|
341
341
|
'回答完成': 'Answer complete',
|
|
342
342
|
'内容较长,生成完成后将分段发送完整回答。':
|
|
343
343
|
'This response is long. The complete answer will be sent in parts when generation finishes.',
|
|
@@ -120,7 +120,8 @@ export default {
|
|
|
120
120
|
'{label}机器人已连接 DeepSeek Harness。': 'The {label} bot is connected to DeepSeek Harness.',
|
|
121
121
|
'/new 开启一个全新会话': '/new Start a brand-new session',
|
|
122
122
|
'/compact 压缩当前会话的较早上下文': '/compact Compact the earlier context of the current session',
|
|
123
|
-
'/workspace
|
|
123
|
+
'/workspace 工作区序号或绝对路径 切换工作区':
|
|
124
|
+
'/workspace <workspace index or absolute path> Switch workspace',
|
|
124
125
|
'/workspacelist 列出工作区绝对路径': '/workspacelist List absolute workspace paths',
|
|
125
126
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题':
|
|
126
127
|
'/sessionlist [workspace index or absolute path] List session IDs and titles',
|
|
@@ -17,7 +17,8 @@ export default {
|
|
|
17
17
|
'当前 Harness Host 上存在的工作区({count}):':
|
|
18
18
|
'Workspaces on the current Harness Host ({count}):',
|
|
19
19
|
'(当前)': '(current)',
|
|
20
|
-
'切换用法:/workspace
|
|
20
|
+
'切换用法:/workspace 工作区序号或绝对路径':
|
|
21
|
+
'To switch: /workspace Workspace index or absolute path',
|
|
21
22
|
'查看会话:/sessionlist 工作区序号或绝对路径':
|
|
22
23
|
'To view Sessions: /sessionlist Workspace index or absolute path',
|
|
23
24
|
'机器人正在移除或已重新接入,无法列出原会话的工作区。':
|
|
@@ -80,10 +81,12 @@ export default {
|
|
|
80
81
|
'归档:{archived}': 'Archived: {archived}',
|
|
81
82
|
'是': 'Yes',
|
|
82
83
|
'否': 'No',
|
|
83
|
-
'用法:/workspace
|
|
84
|
+
'用法:/workspace 工作区序号或绝对路径':
|
|
85
|
+
'Usage: /workspace Workspace index or absolute path',
|
|
84
86
|
'当前机器人暂不支持切换工作区。': 'This bot does not support switching Workspaces yet.',
|
|
85
87
|
'工作区已切换为:{workspace}': 'Workspace switched to: {workspace}',
|
|
86
|
-
'{message}\n用法:/workspace
|
|
88
|
+
'{message}\n用法:/workspace 工作区序号或绝对路径':
|
|
89
|
+
'{message}\nUsage: /workspace Workspace index or absolute path',
|
|
87
90
|
'机器人正在移除或已重新接入,无法切换原会话的工作区。':
|
|
88
91
|
'The bot is being removed or has been reconnected; cannot switch the Workspace of the original Session.',
|
|
89
92
|
|
|
@@ -185,4 +185,6 @@ export default {
|
|
|
185
185
|
'Collected {count}/{limit} messages. The batch is full; send /send or /cancel.',
|
|
186
186
|
'批量内容提交失败,已保留 {count} 条消息。\n请再次发送 /send 重试或 /cancel 取消。':
|
|
187
187
|
'Batch submission failed; {count} messages were retained.\nSend /send to retry or /cancel to cancel.',
|
|
188
|
+
'你可以发送普通消息,但没有执行命令的权限。':
|
|
189
|
+
'You can send regular messages, but you do not have permission to run commands.',
|
|
188
190
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { evaluateAccessPolicy } from './access-policy.mjs';
|
|
2
|
+
import { isSharedLocalCommand } from './command-permission.mjs';
|
|
3
|
+
|
|
4
|
+
export const COMMAND_PERMISSION_DENIED_MESSAGE = '你可以发送普通消息,但没有执行命令的权限。';
|
|
5
|
+
|
|
6
|
+
const POLICY_NOT_CONFIGURED = Object.freeze({ allowed: true, reason: 'policy-not-configured' });
|
|
7
|
+
const PRIVILEGED_SENDER = Object.freeze({ allowed: true, reason: 'privileged-sender' });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read one committed policy snapshot and decide a single inbound event.
|
|
11
|
+
* A missing provider is kept backward-compatible for direct bridge fixtures;
|
|
12
|
+
* production always injects a provider, whose missing/damaged value fails closed.
|
|
13
|
+
*/
|
|
14
|
+
export function evaluateInboundAccess(accessPolicy, {
|
|
15
|
+
conversationType,
|
|
16
|
+
senderIds,
|
|
17
|
+
text = '',
|
|
18
|
+
hasImages = false,
|
|
19
|
+
hasFiles = false,
|
|
20
|
+
isCommand = isSharedLocalCommand(text, { hasImages, hasFiles }),
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (!accessPolicy) return POLICY_NOT_CONFIGURED;
|
|
23
|
+
try {
|
|
24
|
+
if (typeof accessPolicy.isPrivileged === 'function'
|
|
25
|
+
&& accessPolicy.isPrivileged(senderIds, conversationType) === true) {
|
|
26
|
+
return PRIVILEGED_SENDER;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
// A broken privilege lookup must not bypass the persisted policy.
|
|
30
|
+
}
|
|
31
|
+
let policy = null;
|
|
32
|
+
try {
|
|
33
|
+
policy = accessPolicy.getSettings();
|
|
34
|
+
} catch {
|
|
35
|
+
// Provider failures are equivalent to an unavailable persisted policy.
|
|
36
|
+
}
|
|
37
|
+
return evaluateAccessPolicy(policy, {
|
|
38
|
+
conversationType,
|
|
39
|
+
senderIds,
|
|
40
|
+
isCommand,
|
|
41
|
+
...(typeof accessPolicy.equals === 'function' ? { equals: accessPolicy.equals } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_MAX_TITLE_BYTES = 60;
|
|
2
|
+
|
|
3
|
+
const OSC_SEQUENCE = /(?:\u001b\]|\u009d)(?:(?!\u0007|\u001b\\)[\s\S])*(?:\u0007|\u001b\\|$)/gu;
|
|
4
|
+
const CSI_SEQUENCE = /(?:\u001b\[|\u009b)[0-?]*[ -/]*[@-~]/gu;
|
|
5
|
+
const ESC_SEQUENCE = /\u001b[@-_]/gu;
|
|
6
|
+
const CONTROL_CHARACTER = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu;
|
|
7
|
+
const DIRECTIONAL_CONTROL = /[\u200b\u200e\u200f\u202a-\u202e\u2060-\u2064\u2066-\u206f\ufeff]/gu;
|
|
8
|
+
const INJECTED_CONTEXT_PREFIX = /^(?:<dsh_im_source>[\s\S]*?<\/dsh_im_source>\s*)?(?:<dsh_im_source_guidance>[\s\S]*?<\/dsh_im_source_guidance>\s*)?/u;
|
|
9
|
+
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
10
|
+
|
|
11
|
+
function cleanTitleText(input) {
|
|
12
|
+
return input
|
|
13
|
+
.replace(OSC_SEQUENCE, '')
|
|
14
|
+
.replace(CSI_SEQUENCE, '')
|
|
15
|
+
.replace(ESC_SEQUENCE, '')
|
|
16
|
+
.replace(CONTROL_CHARACTER, '')
|
|
17
|
+
.replace(DIRECTIONAL_CONTROL, '')
|
|
18
|
+
.replace(/\s+/gu, ' ')
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function truncateTitle(input, maxBytes = DEFAULT_MAX_TITLE_BYTES) {
|
|
23
|
+
if (Buffer.byteLength(input, 'utf8') <= maxBytes) return input;
|
|
24
|
+
const suffix = '…';
|
|
25
|
+
const budget = maxBytes - Buffer.byteLength(suffix, 'utf8');
|
|
26
|
+
let output = '';
|
|
27
|
+
for (const { segment } of SEGMENTER.segment(input)) {
|
|
28
|
+
if (Buffer.byteLength(output + segment, 'utf8') > budget) break;
|
|
29
|
+
output += segment;
|
|
30
|
+
}
|
|
31
|
+
return `${output.trimEnd()}${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function contentText(content) {
|
|
35
|
+
if (typeof content === 'string') return content;
|
|
36
|
+
if (!Array.isArray(content)) return '';
|
|
37
|
+
return content
|
|
38
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
39
|
+
.map((block) => block.text)
|
|
40
|
+
.join('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fileDisplayName(file) {
|
|
44
|
+
const value = file?.name ?? file?.filename ?? file?.fileName;
|
|
45
|
+
if (typeof value !== 'string') return '';
|
|
46
|
+
return value
|
|
47
|
+
.replaceAll('\\', '/')
|
|
48
|
+
.split('/')
|
|
49
|
+
.at(-1)
|
|
50
|
+
?.replace(/[\u0000-\u001f\u007f]/gu, '')
|
|
51
|
+
.trim() ?? '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Build a deterministic title from the unenhanced first user message. */
|
|
55
|
+
export function initialSessionTitle({ text, content, files } = {}) {
|
|
56
|
+
const original = typeof text === 'string' ? cleanTitleText(text) : '';
|
|
57
|
+
if (original) return truncateTitle(original);
|
|
58
|
+
|
|
59
|
+
// Structured image prompts may only expose their default text through content.
|
|
60
|
+
// Strip only the leading blocks inserted by dsh-im; matching tags later in user
|
|
61
|
+
// content remain ordinary user text.
|
|
62
|
+
const visibleContent = cleanTitleText(
|
|
63
|
+
contentText(content).replace(INJECTED_CONTEXT_PREFIX, ''),
|
|
64
|
+
);
|
|
65
|
+
if (visibleContent) return truncateTitle(visibleContent);
|
|
66
|
+
|
|
67
|
+
const fileName = Array.isArray(files)
|
|
68
|
+
? files.map(fileDisplayName).find(Boolean)
|
|
69
|
+
: '';
|
|
70
|
+
const cleanFileName = fileName ? cleanTitleText(fileName) : '';
|
|
71
|
+
return cleanFileName ? truncateTitle(cleanFileName) : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const INITIAL_SESSION_TITLE_MAX_BYTES = DEFAULT_MAX_TITLE_BYTES;
|