mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
Reuse Host workspaces when Windows path casing differs.
Match workspace.list entries case-insensitively on Win32 so D: vs d: no longer misses an existing workspace and falls through to SESSION_CREATE. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7cdf1d6b94
commit
f1c9abfd1d
6 changed files with 294 additions and 223 deletions
|
|
@ -8,6 +8,9 @@ This file records the notable changes in each dsh-im release. Its format follows
|
||||||
|
|
||||||
### Fixed / 修复
|
### Fixed / 修复
|
||||||
|
|
||||||
|
- Windows 下机器人工作区路径与 Host `workspace.list` 仅盘符/路径大小写不同(如 `D:\…` vs `d:\…`)时,不再因严格字符串匹配漏掉已有工作区而去 `workspace.create` / `session.create`,避免误报 `SESSION_CREATE`。
|
||||||
|
On Windows, when the bot workspace path differs from a Host `workspace.list` entry only by drive/path casing (e.g. `D:\…` vs `d:\…`), IM now reuses that workspace instead of missing it via exact string match and falling through to `workspace.create` / `session.create` (which surfaced as `SESSION_CREATE`).
|
||||||
|
|
||||||
- `session.create` / `workspace.create` 的 Host `internal` 错误不再笼统显示 `HARNESS_SERVICE`:分别映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`,并写入 `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json`(含 method 与脱敏诊断),便于现场定位工作区路径问题。
|
- `session.create` / `workspace.create` 的 Host `internal` 错误不再笼统显示 `HARNESS_SERVICE`:分别映射为 `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`,并写入 `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json`(含 method 与脱敏诊断),便于现场定位工作区路径问题。
|
||||||
Host `internal` failures on `session.create` / `workspace.create` no longer collapse to generic `HARNESS_SERVICE`: they map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`, and write `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json` (method + redacted diagnostics) for field triage of workspace path issues.
|
Host `internal` failures on `session.create` / `workspace.create` no longer collapse to generic `HARNESS_SERVICE`: they map to `SESSION_CREATE` / `WORKSPACE_UNAVAILABLE`, and write `%USERPROFILE%\\.dsh\\integrations\\dsh-im-ops\\last-message-failure.json` (method + redacted diagnostics) for field triage of workspace path issues.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ var React26 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "dsh-im-ops",
|
name: "dsh-im-ops",
|
||||||
version: "4.9.1-ops.33",
|
version: "4.9.1-ops.34",
|
||||||
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
description: "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
414
lib/index.js
414
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-im-ops",
|
"name": "dsh-im-ops",
|
||||||
"version": "4.9.1-ops.33",
|
"version": "4.9.1-ops.34",
|
||||||
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
"description": "Ops fork of dsh-im (all channels kept). Based on @xmanrui/dsh-im@4.9.1 - access/session policies owned here.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -213,13 +213,34 @@ function workspacePaths(value) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows treats drive/path casing as equivalent; Host may store `d:\…`
|
||||||
|
* while the bot UI persists `D:\…`. Match like Win32, not POSIX string equality.
|
||||||
|
*/
|
||||||
|
function sameWorkspacePath(left, right) {
|
||||||
|
if (typeof left !== 'string' || typeof right !== 'string') return false;
|
||||||
|
const wanted = left.trim();
|
||||||
|
const got = right.trim();
|
||||||
|
if (!wanted || !got) return false;
|
||||||
|
const a = resolve(wanted);
|
||||||
|
const b = resolve(got);
|
||||||
|
return process.platform === 'win32'
|
||||||
|
? a.toLowerCase() === b.toLowerCase()
|
||||||
|
: a === b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findListedWorkspace(items, workspacePath) {
|
||||||
|
if (!Array.isArray(items)) return null;
|
||||||
|
return items.find((item) => sameWorkspacePath(item?.path, workspacePath)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
function workspaceFromList(workspacePath, workspaceList) {
|
function workspaceFromList(workspacePath, workspaceList) {
|
||||||
if (!Array.isArray(workspaceList?.items)
|
if (!Array.isArray(workspaceList?.items)
|
||||||
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
|| !Array.isArray(workspaceList?.archivedSessionIds)) {
|
||||||
throw new Error('Harness returned an invalid response for workspace.list');
|
throw new Error('Harness returned an invalid response for workspace.list');
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = workspaceList.items.find((item) => item?.path === workspacePath);
|
const workspace = findListedWorkspace(workspaceList.items, workspacePath);
|
||||||
if (!workspace) return null;
|
if (!workspace) return null;
|
||||||
if (!Array.isArray(workspace.sessionIds)
|
if (!Array.isArray(workspace.sessionIds)
|
||||||
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
|| workspace.sessionIds.some((sessionId) => typeof sessionId !== 'string')) {
|
||||||
|
|
@ -910,7 +931,8 @@ export class HarnessClient {
|
||||||
async workspaceId(options = {}) {
|
async workspaceId(options = {}) {
|
||||||
const { workspace = this.#workspace, ...rpcOptions } = options;
|
const { workspace = this.#workspace, ...rpcOptions } = options;
|
||||||
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
||||||
const existing = items.find((item) => item.path === workspace);
|
// Reuse Host's listed entry under Win32 path-case aliases (D: vs d:).
|
||||||
|
const existing = findListedWorkspace(items, workspace);
|
||||||
if (existing) return existing.workspaceId;
|
if (existing) return existing.workspaceId;
|
||||||
const created = await this.rpc('workspace.create', { path: workspace }, 30_000, rpcOptions);
|
const created = await this.rpc('workspace.create', { path: workspace }, 30_000, rpcOptions);
|
||||||
return created.workspace.workspaceId;
|
return created.workspace.workspaceId;
|
||||||
|
|
@ -989,13 +1011,6 @@ export class HarnessClient {
|
||||||
if (typeof sessionId !== 'string' || !sessionId) return false;
|
if (typeof sessionId !== 'string' || !sessionId) return false;
|
||||||
if (typeof workspacePath !== 'string' || !workspacePath.trim()) return false;
|
if (typeof workspacePath !== 'string' || !workspacePath.trim()) return false;
|
||||||
const wanted = resolve(workspacePath.trim());
|
const wanted = resolve(workspacePath.trim());
|
||||||
const samePath = (value) => {
|
|
||||||
if (typeof value !== 'string' || !value.trim()) return false;
|
|
||||||
const got = resolve(value.trim());
|
|
||||||
return process.platform === 'win32'
|
|
||||||
? got.toLowerCase() === wanted.toLowerCase()
|
|
||||||
: got === wanted;
|
|
||||||
};
|
|
||||||
|
|
||||||
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
const sessionList = await this.rpc('session.list', {}, 30_000, options);
|
||||||
if (!sessionList || typeof sessionList !== 'object' || !Array.isArray(sessionList.items)) {
|
if (!sessionList || typeof sessionList !== 'object' || !Array.isArray(sessionList.items)) {
|
||||||
|
|
@ -1003,13 +1018,12 @@ export class HarnessClient {
|
||||||
}
|
}
|
||||||
const item = sessionList.items.find((entry) => entry?.sessionId === sessionId);
|
const item = sessionList.items.find((entry) => entry?.sessionId === sessionId);
|
||||||
if (!item) return false;
|
if (!item) return false;
|
||||||
if (typeof item.cwd === 'string' && item.cwd.trim()) return samePath(item.cwd);
|
if (typeof item.cwd === 'string' && item.cwd.trim()) {
|
||||||
|
return sameWorkspacePath(item.cwd, wanted);
|
||||||
|
}
|
||||||
|
|
||||||
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
const workspaceList = await this.rpc('workspace.list', {}, 30_000, options);
|
||||||
const workspace = workspaceFromList(wanted, workspaceList)
|
const workspace = workspaceFromList(wanted, workspaceList);
|
||||||
?? (Array.isArray(workspaceList?.items)
|
|
||||||
? workspaceList.items.find((entry) => samePath(entry?.path))
|
|
||||||
: null);
|
|
||||||
if (!workspace) return false;
|
if (!workspace) return false;
|
||||||
return Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(sessionId);
|
return Array.isArray(workspace.sessionIds) && workspace.sessionIds.includes(sessionId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -594,6 +594,60 @@ test('HarnessClient reads the nested workspace.create response used by DSH rc.6'
|
||||||
assert.deepEqual(methods, ['workspace.list', 'workspace.create']);
|
assert.deepEqual(methods, ['workspace.list', 'workspace.create']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('HarnessClient reuses Host workspaces when only Windows path casing differs', async (t) => {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
t.skip('Win32 path-case aliasing only applies on Windows');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods = [];
|
||||||
|
t.mock.method(globalThis, 'fetch', async (_url, options) => {
|
||||||
|
const request = JSON.parse(options.body);
|
||||||
|
methods.push(request.method);
|
||||||
|
assert.notEqual(request.method, 'workspace.create');
|
||||||
|
const value = request.method === 'session.list'
|
||||||
|
? {
|
||||||
|
items: [{
|
||||||
|
sessionId: 'session-one',
|
||||||
|
cwd: 'd:\\ai_code\\dsh',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
items: [{
|
||||||
|
workspaceId: 'workspace-existing',
|
||||||
|
path: 'd:\\ai_code\\dsh',
|
||||||
|
sessionIds: ['session-one'],
|
||||||
|
}],
|
||||||
|
archivedSessionIds: [],
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
async json() {
|
||||||
|
return {
|
||||||
|
type: 'server-response',
|
||||||
|
rpcId: request.rpcId,
|
||||||
|
result: { ok: true, value },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new HarnessClient({
|
||||||
|
baseUrl: 'http://127.0.0.1:3080',
|
||||||
|
workspace: 'D:\\ai_code\\dsh',
|
||||||
|
agentPreset: 'standard',
|
||||||
|
autostart: false,
|
||||||
|
dshBin: 'dsh',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await client.workspaceId(), 'workspace-existing');
|
||||||
|
assert.deepEqual(methods, ['workspace.list']);
|
||||||
|
assert.equal(
|
||||||
|
await client.sessionBelongsToWorkspace('session-one', 'D:\\ai_code\\dsh'),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('HarnessClient asks do not control file-return tool availability', async () => {
|
test('HarnessClient asks do not control file-return tool availability', async () => {
|
||||||
const client = new HarnessClient({
|
const client = new HarnessClient({
|
||||||
baseUrl: 'http://127.0.0.1:3080',
|
baseUrl: 'http://127.0.0.1:3080',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue