@xmanrui/dsh-im 4.24.0 → 4.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -1
- package/README.md +5 -1
- package/lib/client.js +407 -334
- package/lib/index.js +194 -194
- package/package.json +5 -1
- package/plugin-src/client/channel-card-meta.js +3 -9
- package/plugin-src/client/channels/dingtalk/index.js +9 -8
- package/plugin-src/client/channels/feishu/index.js +14 -13
- package/plugin-src/client/channels/qq/index.js +9 -8
- package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
- package/plugin-src/client/channels/shared/token-channel.js +9 -8
- package/plugin-src/client/channels/wecom/index.js +9 -8
- package/plugin-src/client/channels/wecom-app/index.js +9 -8
- package/plugin-src/client/channels/weixin/index.js +9 -8
- package/plugin-src/client/channels/whatsapp/index.js +9 -8
- package/plugin-src/client/i18n.js +5 -0
- package/plugin-src/client/styles.js +10 -6
- package/plugin-src/client/update-panel.js +18 -8
- package/plugin-src/host/update-service.mjs +19 -14
- package/src/channels/feishu/bridge.mjs +91 -42
- package/src/channels/shared/workspace-session.mjs +7 -1
|
@@ -113,7 +113,7 @@ function processAlive(pid) {
|
|
|
113
113
|
function publicJob(job) {
|
|
114
114
|
if (!job) return null;
|
|
115
115
|
const { id, state, targetVersion, message } = job;
|
|
116
|
-
return { id, state, targetVersion, message };
|
|
116
|
+
return { id, state, targetVersion, message, recoverable: job.recoverable === true };
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** One update job per profile. Persist intent before starting pnpm, and never apply a restart here. */
|
|
@@ -158,27 +158,32 @@ export function createUpdateService({
|
|
|
158
158
|
if (!JOB_STATES.has(job.state) || typeof job.id !== 'string' || !semver.valid(job.targetVersion)) {
|
|
159
159
|
throw updateError('state-unavailable');
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
// Recovery is derived from the current installation, never a persisted flag.
|
|
162
|
+
job = { ...job, recoverable: false };
|
|
163
|
+
const historical = job.id !== activeJob?.id;
|
|
164
|
+
if (ACTIVE_STATES.has(job.state) && historical) {
|
|
162
165
|
if (lock === NO_LOCK || lock?.id !== job.id || !processAlive(lock?.pid)) {
|
|
163
166
|
job = { ...job, state: 'interrupted', message: 'recovery-required' };
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
// A later Host can retry after a verified manual repair. Never infer this
|
|
169
|
-
// from version equality while an old process lock is still present.
|
|
170
|
-
if (['failed', 'interrupted'].includes(job.state) && environment.packageValid === true
|
|
171
|
-
&& environment.installedVersion === runningVersion && !environment.blockedReason) {
|
|
172
|
-
return job.targetVersion === runningVersion
|
|
173
|
-
? { ...job, state: 'completed', message: 'recovered' }
|
|
174
|
-
: { ...job, recoverable: true };
|
|
175
|
-
}
|
|
169
|
+
if (historical && !ACTIVE_STATES.has(job.state) && lock !== NO_LOCK) {
|
|
170
|
+
return { ...job, state: 'interrupted', message: 'recovery-required' };
|
|
176
171
|
}
|
|
177
172
|
if (job.state === 'restart-required' || job.state === 'completed') {
|
|
178
173
|
if (environment.installedVersion !== job.targetVersion || environment.packageValid !== true) {
|
|
179
|
-
|
|
174
|
+
job = { ...job, state: 'interrupted', message: 'installation-changed' };
|
|
175
|
+
} else {
|
|
176
|
+
job = { ...job, state: runningVersion === job.targetVersion ? 'completed' : 'restart-required' };
|
|
180
177
|
}
|
|
181
|
-
|
|
178
|
+
}
|
|
179
|
+
// Reconcile after deriving the job state so an external upgrade or rollback
|
|
180
|
+
// can also recover. Keep history read-only and do not claim its target ran.
|
|
181
|
+
if (historical && lock === NO_LOCK && ['failed', 'interrupted'].includes(job.state)
|
|
182
|
+
&& environment.packageValid === true && environment.eligible === true
|
|
183
|
+
&& environment.installedVersion === runningVersion && !environment.blockedReason) {
|
|
184
|
+
return job.targetVersion === runningVersion
|
|
185
|
+
? { ...job, state: 'completed', message: 'recovered' }
|
|
186
|
+
: { ...job, recoverable: true };
|
|
182
187
|
}
|
|
183
188
|
return job;
|
|
184
189
|
}
|
|
@@ -124,6 +124,7 @@ const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
|
|
|
124
124
|
const ROOT_CANDIDATE_TTL_MS = 30 * 60_000;
|
|
125
125
|
/** Bound main-feed roots awaiting a topic so silent drops cannot grow them. */
|
|
126
126
|
const MAX_ROOT_CANDIDATES = 512;
|
|
127
|
+
const MAX_TOPIC_ANCHORS = 4_000;
|
|
127
128
|
|
|
128
129
|
const MENU_COMMAND = /^\/m(?:enu)?$/i;
|
|
129
130
|
const REPAIR_COMMAND_PREFIX = /^\/repair(?:\s|$)/i;
|
|
@@ -624,6 +625,8 @@ export class FeishuHarnessBridge {
|
|
|
624
625
|
#anchorTopicReply = new Map();
|
|
625
626
|
/** Main-feed roots this bot intends to auto-open as topics, awaiting thread_id. */
|
|
626
627
|
#rootCandidates = new Map();
|
|
628
|
+
/** Last successfully injected topic root per conversation and AI session. */
|
|
629
|
+
#topicAnchors = new Map();
|
|
627
630
|
#repair;
|
|
628
631
|
#repairAttempt = null;
|
|
629
632
|
#repairMonitorVersion = 0;
|
|
@@ -4875,6 +4878,72 @@ export class FeishuHarnessBridge {
|
|
|
4875
4878
|
};
|
|
4876
4879
|
}
|
|
4877
4880
|
|
|
4881
|
+
async #preparePrompt(event, key, message) {
|
|
4882
|
+
const snapshot = this.#acceptedMessageIds.get(event.message.message_id);
|
|
4883
|
+
const build = async (input) => {
|
|
4884
|
+
const rich = hasInboundImages(input) || hasReplyReference(input);
|
|
4885
|
+
const original = rich
|
|
4886
|
+
? await promptContentForInboundMessage(input, { signal: this.#signal, deferImages: true })
|
|
4887
|
+
: input.content;
|
|
4888
|
+
const enhanced = enhanceContextContent(original, snapshot, () => ({
|
|
4889
|
+
channel: 'feishu',
|
|
4890
|
+
senderId: senderOpenId(event),
|
|
4891
|
+
chatId: event.message.chat_id,
|
|
4892
|
+
threadId: event.message.thread_id,
|
|
4893
|
+
}));
|
|
4894
|
+
return {
|
|
4895
|
+
content: rich || enhanced !== original ? enhanced : undefined,
|
|
4896
|
+
contextEnhanced: enhanced !== original,
|
|
4897
|
+
sourceGuidance: snapshot?.config?.guidance,
|
|
4898
|
+
};
|
|
4899
|
+
};
|
|
4900
|
+
const rootId = nonEmptyString(event.message.root_id);
|
|
4901
|
+
const topicAnchor = event.message.chat_type === 'group'
|
|
4902
|
+
&& nonEmptyString(event.message.thread_id)
|
|
4903
|
+
&& rootId && message.replyTo?.messageId === rootId
|
|
4904
|
+
// A reference-only message must not become an empty prompt.
|
|
4905
|
+
&& (nonEmptyString(message.content) || hasInboundImages(message) || hasInboundFiles(message));
|
|
4906
|
+
if (!topicAnchor) return build(message);
|
|
4907
|
+
|
|
4908
|
+
const withoutAnchor = { ...message };
|
|
4909
|
+
delete withoutAnchor.replyTo;
|
|
4910
|
+
const base = await build(withoutAnchor);
|
|
4911
|
+
let preparedSessionId;
|
|
4912
|
+
let anchorLoaded = false;
|
|
4913
|
+
return {
|
|
4914
|
+
...base,
|
|
4915
|
+
prepareContent: async ({ sessionId }) => {
|
|
4916
|
+
preparedSessionId = sessionId;
|
|
4917
|
+
anchorLoaded = false;
|
|
4918
|
+
const previous = this.#topicAnchors.get(key);
|
|
4919
|
+
if (previous?.sessionId === sessionId && previous.rootMessageId === rootId) {
|
|
4920
|
+
return base.content ?? message.content;
|
|
4921
|
+
}
|
|
4922
|
+
const prepared = await build({
|
|
4923
|
+
...message,
|
|
4924
|
+
replyTo: {
|
|
4925
|
+
...message.replyTo,
|
|
4926
|
+
load: async (options) => {
|
|
4927
|
+
const reference = await message.replyTo.load(options);
|
|
4928
|
+
anchorLoaded = !reference?.unavailableReason && Boolean(
|
|
4929
|
+
nonEmptyString(reference?.content) || reference?.attachments?.length,
|
|
4930
|
+
);
|
|
4931
|
+
return reference;
|
|
4932
|
+
},
|
|
4933
|
+
},
|
|
4934
|
+
});
|
|
4935
|
+
return prepared.content;
|
|
4936
|
+
},
|
|
4937
|
+
onAskComplete: (sessionId) => {
|
|
4938
|
+
if (!anchorLoaded || sessionId !== preparedSessionId) return;
|
|
4939
|
+
if (!this.#topicAnchors.has(key) && this.#topicAnchors.size >= MAX_TOPIC_ANCHORS) {
|
|
4940
|
+
this.#topicAnchors.delete(this.#topicAnchors.keys().next().value);
|
|
4941
|
+
}
|
|
4942
|
+
this.#topicAnchors.set(key, { sessionId, rootMessageId: rootId });
|
|
4943
|
+
},
|
|
4944
|
+
};
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4878
4947
|
/**
|
|
4879
4948
|
* 分步直推:`#answerWithStream` 流式分支在开关开启时的完整替代路径。
|
|
4880
4949
|
* 过程(工具调用、助手中间说明)与最终答案均以富文本 post 逐条直推
|
|
@@ -4886,30 +4955,18 @@ export class FeishuHarnessBridge {
|
|
|
4886
4955
|
const messageId = event.message.message_id;
|
|
4887
4956
|
const text = message.content;
|
|
4888
4957
|
let askCompleted = false;
|
|
4889
|
-
const markAskComplete = () => {
|
|
4958
|
+
const markAskComplete = (sessionId) => {
|
|
4890
4959
|
if (askCompleted) return;
|
|
4891
4960
|
askCompleted = true;
|
|
4961
|
+
prompt.onAskComplete?.(sessionId);
|
|
4892
4962
|
this.#endImTurn(key);
|
|
4893
4963
|
onAskComplete?.();
|
|
4894
4964
|
};
|
|
4895
4965
|
// 与流式分支一致的提示内容构造:图片与回复引用展开为富提示内容,已接受
|
|
4896
4966
|
// 的上下文增强按原样重放。直推分流发生在 `#answerWithStream` 构造之前,
|
|
4897
4967
|
// 这里就是本回合唯一一次构造(无重复的 prompt 往返)。
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
: undefined;
|
|
4901
|
-
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
4902
|
-
let contextEnhanced = false;
|
|
4903
|
-
if (snapshot) {
|
|
4904
|
-
const originalContent = content ?? text;
|
|
4905
|
-
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
4906
|
-
channel: 'feishu',
|
|
4907
|
-
senderId: senderOpenId(event),
|
|
4908
|
-
chatId: event.message.chat_id,
|
|
4909
|
-
threadId: event.message.thread_id,
|
|
4910
|
-
}));
|
|
4911
|
-
contextEnhanced = content !== originalContent;
|
|
4912
|
-
}
|
|
4968
|
+
const prompt = await this.#preparePrompt(event, key, message);
|
|
4969
|
+
const { content, contextEnhanced } = prompt;
|
|
4913
4970
|
// Every turn reopens the per-conversation circuit breaker.
|
|
4914
4971
|
const priorState = this.#stepPushSendState.get(key);
|
|
4915
4972
|
if (this.#stepPushSendState.size >= MAX_STEP_PUSH_SEND_STATES && !priorState) {
|
|
@@ -5010,7 +5067,8 @@ export class FeishuHarnessBridge {
|
|
|
5010
5067
|
text,
|
|
5011
5068
|
content,
|
|
5012
5069
|
titleText: event.batchSubmission?.title,
|
|
5013
|
-
sourceGuidance:
|
|
5070
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5071
|
+
prepareContent: prompt.prepareContent,
|
|
5014
5072
|
contextEnhanced,
|
|
5015
5073
|
createOptions: { signal: this.#signal },
|
|
5016
5074
|
existsOptions: { signal: this.#signal },
|
|
@@ -5089,8 +5147,8 @@ export class FeishuHarnessBridge {
|
|
|
5089
5147
|
// 回合结束(成功/失败/中断):停看门狗并撤回残留思考中心跳。
|
|
5090
5148
|
await watchdog?.stop();
|
|
5091
5149
|
}
|
|
5150
|
+
markAskComplete(completed.sessionId);
|
|
5092
5151
|
await cot?.finish();
|
|
5093
|
-
markAskComplete();
|
|
5094
5152
|
const finalStepText = pendingStep ? pendingStep.text : null;
|
|
5095
5153
|
pendingStep = null;
|
|
5096
5154
|
const finalText = (typeof finalStepText === 'string' && finalStepText.trim())
|
|
@@ -5228,9 +5286,10 @@ export class FeishuHarnessBridge {
|
|
|
5228
5286
|
const messageId = event.message.message_id;
|
|
5229
5287
|
const text = message.content;
|
|
5230
5288
|
let askCompleted = false;
|
|
5231
|
-
const markAskComplete = () => {
|
|
5289
|
+
const markAskComplete = (sessionId) => {
|
|
5232
5290
|
if (askCompleted) return;
|
|
5233
5291
|
askCompleted = true;
|
|
5292
|
+
prompt.onAskComplete?.(sessionId);
|
|
5234
5293
|
this.#endImTurn(key);
|
|
5235
5294
|
onAskComplete?.();
|
|
5236
5295
|
};
|
|
@@ -5241,23 +5300,10 @@ export class FeishuHarnessBridge {
|
|
|
5241
5300
|
if (this.#stepPush && this.#channel?.stream) {
|
|
5242
5301
|
return this.#answerWithStepPush(event, key, message, { onAskComplete });
|
|
5243
5302
|
}
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
: undefined;
|
|
5247
|
-
const snapshot = this.#acceptedMessageIds.get(messageId);
|
|
5248
|
-
let contextEnhanced = false;
|
|
5249
|
-
if (snapshot) {
|
|
5250
|
-
const originalContent = content ?? text;
|
|
5251
|
-
content = enhanceContextContent(originalContent, snapshot, () => ({
|
|
5252
|
-
channel: 'feishu',
|
|
5253
|
-
senderId: senderOpenId(event),
|
|
5254
|
-
chatId: event.message.chat_id,
|
|
5255
|
-
threadId: event.message.thread_id,
|
|
5256
|
-
}));
|
|
5257
|
-
contextEnhanced = content !== originalContent;
|
|
5258
|
-
}
|
|
5303
|
+
const prompt = await this.#preparePrompt(event, key, message);
|
|
5304
|
+
const { content, contextEnhanced } = prompt;
|
|
5259
5305
|
if (!this.#channel?.stream) {
|
|
5260
|
-
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5306
|
+
const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5261
5307
|
deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
|
|
5262
5308
|
harness: this.#harness,
|
|
5263
5309
|
state: this.#state,
|
|
@@ -5265,13 +5311,14 @@ export class FeishuHarnessBridge {
|
|
|
5265
5311
|
text,
|
|
5266
5312
|
content,
|
|
5267
5313
|
titleText: event.batchSubmission?.title,
|
|
5268
|
-
sourceGuidance:
|
|
5314
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5315
|
+
prepareContent: prompt.prepareContent,
|
|
5269
5316
|
contextEnhanced,
|
|
5270
5317
|
createOptions: { signal: this.#signal },
|
|
5271
5318
|
existsOptions: { signal: this.#signal },
|
|
5272
5319
|
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5273
5320
|
});
|
|
5274
|
-
markAskComplete();
|
|
5321
|
+
markAskComplete(sessionId);
|
|
5275
5322
|
let textReceipt;
|
|
5276
5323
|
let textSendError = null;
|
|
5277
5324
|
try {
|
|
@@ -5346,13 +5393,14 @@ export class FeishuHarnessBridge {
|
|
|
5346
5393
|
text,
|
|
5347
5394
|
content,
|
|
5348
5395
|
titleText: event.batchSubmission?.title,
|
|
5349
|
-
sourceGuidance:
|
|
5396
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5397
|
+
prepareContent: prompt.prepareContent,
|
|
5350
5398
|
contextEnhanced,
|
|
5351
5399
|
createOptions: { signal: this.#signal },
|
|
5352
5400
|
existsOptions: { signal: this.#signal },
|
|
5353
5401
|
askOptions,
|
|
5354
5402
|
});
|
|
5355
|
-
markAskComplete();
|
|
5403
|
+
markAskComplete(completed.sessionId);
|
|
5356
5404
|
completedAnswer = completed.answer;
|
|
5357
5405
|
completedArtifacts = completed.artifacts ?? [];
|
|
5358
5406
|
await controller.setContent(answerTextForDelivery(completedAnswer, completedArtifacts));
|
|
@@ -5410,7 +5458,7 @@ export class FeishuHarnessBridge {
|
|
|
5410
5458
|
if (promptStarted) throw error;
|
|
5411
5459
|
|
|
5412
5460
|
this.#logger.warn?.('[dsh-feishu] native stream unavailable; using text fallback:', error.message);
|
|
5413
|
-
const { answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5461
|
+
const { sessionId, answer, artifacts = [] } = await askInWorkspaceSession({
|
|
5414
5462
|
deferredDelivery: () => ({ coordinator: this.#deferred, chatId, replyToMessageId: messageId }),
|
|
5415
5463
|
harness: this.#harness,
|
|
5416
5464
|
state: this.#state,
|
|
@@ -5418,13 +5466,14 @@ export class FeishuHarnessBridge {
|
|
|
5418
5466
|
text,
|
|
5419
5467
|
content,
|
|
5420
5468
|
titleText: event.batchSubmission?.title,
|
|
5421
|
-
sourceGuidance:
|
|
5469
|
+
sourceGuidance: prompt.sourceGuidance,
|
|
5470
|
+
prepareContent: prompt.prepareContent,
|
|
5422
5471
|
contextEnhanced,
|
|
5423
5472
|
createOptions: { signal: this.#signal },
|
|
5424
5473
|
existsOptions: { signal: this.#signal },
|
|
5425
5474
|
askOptions: this.#interactionAskOptions(event, key, message.files, message.images),
|
|
5426
5475
|
});
|
|
5427
|
-
markAskComplete();
|
|
5476
|
+
markAskComplete(sessionId);
|
|
5428
5477
|
let textReceipt;
|
|
5429
5478
|
let textSendError = null;
|
|
5430
5479
|
try {
|
|
@@ -94,6 +94,7 @@ export async function askInWorkspaceSession({
|
|
|
94
94
|
key,
|
|
95
95
|
text,
|
|
96
96
|
content,
|
|
97
|
+
prepareContent,
|
|
97
98
|
titleText,
|
|
98
99
|
sourceGuidance,
|
|
99
100
|
contextEnhanced = false,
|
|
@@ -165,8 +166,13 @@ export async function askInWorkspaceSession({
|
|
|
165
166
|
await originalOnArtifact?.(artifact);
|
|
166
167
|
};
|
|
167
168
|
let answer;
|
|
169
|
+
// Prepare session-dependent context after binding, outside the binding
|
|
170
|
+
// lock. A stale-workspace retry must prepare it for the new session too.
|
|
171
|
+
const prompt = prepareContent
|
|
172
|
+
? await prepareContent({ sessionId: binding.sessionId, content, text })
|
|
173
|
+
: content ?? text;
|
|
168
174
|
try {
|
|
169
|
-
answer = await binding.session.ask(
|
|
175
|
+
answer = await binding.session.ask(prompt, artifactOptions);
|
|
170
176
|
} catch (error) {
|
|
171
177
|
if (error?.code === 'harness-reply-timeout' && deferredDelivery) {
|
|
172
178
|
try {
|