@fa-software/semi2-dsh 0.0.10-preview.20260920042705
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/cordis.patch.yml +5 -0
- package/lib/client.js +351 -0
- package/lib/client.js.map +1 -0
- package/lib/codegraph/cli.js +142 -0
- package/lib/codegraph/config.js +22 -0
- package/lib/codegraph/ensure.js +25 -0
- package/lib/codegraph/guide.js +21 -0
- package/lib/codegraph/index.js +97 -0
- package/lib/codegraph/tools.js +227 -0
- package/lib/codegraph/workspace.js +52 -0
- package/lib/configuration/compile.js +177 -0
- package/lib/configuration/credentials.js +104 -0
- package/lib/configuration/generated/graphql.js +1 -0
- package/lib/configuration/generated/schema.js +95 -0
- package/lib/configuration/graphql.js +193 -0
- package/lib/configuration/index.js +29 -0
- package/lib/configuration/login.js +96 -0
- package/lib/configuration/runtime.js +1100 -0
- package/lib/configuration/skill-resources.js +218 -0
- package/lib/configuration/skills.js +185 -0
- package/lib/configuration/status.js +57 -0
- package/lib/configuration/workspace.js +55 -0
- package/lib/configuration/workspaces.js +169 -0
- package/lib/contract.js +42 -0
- package/lib/index.js +14 -0
- package/lib/types/codegraph/cli.d.ts +20 -0
- package/lib/types/codegraph/config.d.ts +10 -0
- package/lib/types/codegraph/ensure.d.ts +3 -0
- package/lib/types/codegraph/guide.d.ts +4 -0
- package/lib/types/codegraph/index.d.ts +5 -0
- package/lib/types/codegraph/tools.d.ts +17 -0
- package/lib/types/codegraph/workspace.d.ts +8 -0
- package/lib/types/configuration/compile.d.ts +81 -0
- package/lib/types/configuration/credentials.d.ts +35 -0
- package/lib/types/configuration/generated/graphql.d.ts +100 -0
- package/lib/types/configuration/generated/schema.d.ts +1003 -0
- package/lib/types/configuration/graphql.d.ts +46 -0
- package/lib/types/configuration/index.d.ts +9 -0
- package/lib/types/configuration/login.d.ts +22 -0
- package/lib/types/configuration/runtime.d.ts +320 -0
- package/lib/types/configuration/skill-resources.d.ts +14 -0
- package/lib/types/configuration/skills.d.ts +8 -0
- package/lib/types/configuration/status.d.ts +9 -0
- package/lib/types/configuration/workspace.d.ts +7 -0
- package/lib/types/configuration/workspaces.d.ts +96 -0
- package/lib/types/contract.d.ts +32 -0
- package/lib/types/index.d.ts +11 -0
- package/lib/types/web/index.d.ts +2 -0
- package/lib/types/web/login.d.ts +2 -0
- package/lib/types/web/loopback-bridge.d.ts +8 -0
- package/lib/types/web/preview-proxy.d.ts +17 -0
- package/lib/types/web/runtime.d.ts +8 -0
- package/lib/types/web/welcome-ack.d.ts +33 -0
- package/lib/web/index.js +38 -0
- package/lib/web/login.js +79 -0
- package/lib/web/loopback-bridge.js +12 -0
- package/lib/web/preview-proxy.js +201 -0
- package/lib/web/runtime.js +19 -0
- package/lib/web/welcome-ack.js +51 -0
- package/package.json +81 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/**
|
|
3
|
+
* dsh-client-ui-settings-models 中 bundle 私有常量的镜像(比 notice 文案高一层):升级 pinned dsh channel
|
|
4
|
+
* (DSH_VERSION)时须核对该串仍等于客户端 bundle 里的值,回归测试钉住它;陈旧值只退化为原生行为。
|
|
5
|
+
*/
|
|
6
|
+
export declare const WELCOME_NOTICE_VERSION = "2026-08-13.1";
|
|
7
|
+
/** 产品级 GUI onboarding 事实的 durable settings 命名空间。 */
|
|
8
|
+
export declare const WELCOME_NOTICE_NAMESPACE = "ui-onboarding";
|
|
9
|
+
/** 记录已确认的欢迎声明文案版本。 */
|
|
10
|
+
export declare const WELCOME_NOTICE_ACK_FIELD = "welcomeNoticeVersion";
|
|
11
|
+
/** 本模块依赖的最小 host settings 服务面。 */
|
|
12
|
+
export interface SettingsLike {
|
|
13
|
+
describe(): Array<{
|
|
14
|
+
ns: string;
|
|
15
|
+
}>;
|
|
16
|
+
update(ns: string, patch: object): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface SeedOptions {
|
|
19
|
+
/** 该命名空间被注册即视为 settings 服务就绪。 */
|
|
20
|
+
namespace?: string;
|
|
21
|
+
/** 两次就绪探测之间的间隔(ms)。 */
|
|
22
|
+
intervalMs?: number;
|
|
23
|
+
/** 放弃前的最多探测次数(默认 50ms × 40 ≈ 2s)。 */
|
|
24
|
+
attempts?: number;
|
|
25
|
+
/** 替换轮询等待的测试接缝。 */
|
|
26
|
+
wait?: (ms: number) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare const SEED_INTERVAL_MS = 50;
|
|
29
|
+
export declare const SEED_ATTEMPTS = 40;
|
|
30
|
+
/** 等 "ui-onboarding" 注册后并入当前版本号(幂等);拒绝/失败会 reject 交调用方降级,命名空间始终未就绪返回 false。 */
|
|
31
|
+
export declare function seedWelcomeAcknowledgement(settings: SettingsLike, options?: SeedOptions): Promise<boolean>;
|
|
32
|
+
/** 在可选 `settings` 服务上 best-effort 预置:只降级。 */
|
|
33
|
+
export declare function installWelcomeAck(ctx: Context): void;
|
package/lib/web/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { installLoginRoute } from './login.js';
|
|
2
|
+
import { installLoopbackBridge } from './loopback-bridge.js';
|
|
3
|
+
import { installPreviewProxy } from './preview-proxy.js';
|
|
4
|
+
import { createWorkstationRuntime } from './runtime.js';
|
|
5
|
+
import { installWelcomeAck } from './welcome-ack.js';
|
|
6
|
+
function warn(ctx, message) {
|
|
7
|
+
try {
|
|
8
|
+
ctx.logger('semi2-web').warn(message);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// 无 logger 的宿主(单测)忽略。
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function reason(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
export function installWebSurface(ctx) {
|
|
18
|
+
ctx.inject(['webServer'], (workstationCtx) => {
|
|
19
|
+
// 与预览面无关的 web 面预置先装:工作区运行时不可用也不该连带跳过。
|
|
20
|
+
installWelcomeAck(workstationCtx);
|
|
21
|
+
let runtime;
|
|
22
|
+
try {
|
|
23
|
+
const webServer = workstationCtx.get('webServer');
|
|
24
|
+
runtime = createWorkstationRuntime(webServer.port);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
warn(workstationCtx, `工作站运行时不可用,跳过预览面:${reason(error)}`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
installLoopbackBridge(workstationCtx);
|
|
31
|
+
installLoginRoute(workstationCtx);
|
|
32
|
+
workstationCtx.inject(['webRuntime', 'connection'], (webCtx) => {
|
|
33
|
+
void installPreviewProxy(webCtx, runtime).catch((error) => {
|
|
34
|
+
warn(webCtx, `预览反代启动失败:${reason(error)}`);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
package/lib/web/login.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const LOGIN_PATH = '/semi2/login';
|
|
2
|
+
const LOGOUT_PATH = '/semi2/logout';
|
|
3
|
+
const MAX_BODY = 8 * 1024;
|
|
4
|
+
function send(res, status, body) {
|
|
5
|
+
const payload = JSON.stringify(body);
|
|
6
|
+
res.writeHead(status, {
|
|
7
|
+
'content-type': 'application/json; charset=utf-8',
|
|
8
|
+
'cache-control': 'no-store',
|
|
9
|
+
'content-length': Buffer.byteLength(payload),
|
|
10
|
+
});
|
|
11
|
+
res.end(payload);
|
|
12
|
+
}
|
|
13
|
+
async function readJson(req) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let size = 0;
|
|
16
|
+
for await (const chunk of req) {
|
|
17
|
+
const buffer = chunk instanceof Uint8Array ? chunk : Buffer.from(chunk);
|
|
18
|
+
size += buffer.byteLength;
|
|
19
|
+
if (size > MAX_BODY)
|
|
20
|
+
throw new Error('BODY_TOO_LARGE');
|
|
21
|
+
chunks.push(buffer);
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
24
|
+
}
|
|
25
|
+
function text(value) {
|
|
26
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
27
|
+
}
|
|
28
|
+
export function installLoginRoute(ctx) {
|
|
29
|
+
const webServer = ctx.get('webServer');
|
|
30
|
+
const config = ctx.get('semi2Config');
|
|
31
|
+
if (webServer === undefined || config === undefined)
|
|
32
|
+
return;
|
|
33
|
+
ctx.effect(() => webServer.register({
|
|
34
|
+
kind: 'exact',
|
|
35
|
+
path: LOGIN_PATH,
|
|
36
|
+
handler: async (req, res) => {
|
|
37
|
+
if (req.method !== 'POST') {
|
|
38
|
+
send(res, 405, { code: 'METHOD_NOT_ALLOWED' });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readJson(req);
|
|
43
|
+
const body = raw !== null && typeof raw === 'object'
|
|
44
|
+
? raw
|
|
45
|
+
: {};
|
|
46
|
+
const url = text(body.url);
|
|
47
|
+
const username = text(body.username);
|
|
48
|
+
const password = typeof body.password === 'string' ? body.password : '';
|
|
49
|
+
if (url === '' || username === '' || password === '')
|
|
50
|
+
throw Object.assign(new Error('INVALID_INPUT'), { code: 'INVALID_INPUT' });
|
|
51
|
+
const result = await config.login({ url, username, password });
|
|
52
|
+
send(res, 200, { ok: true, userName: result.userName });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const code = error !== null && typeof error === 'object' && 'code' in error
|
|
56
|
+
? String(error.code)
|
|
57
|
+
: 'LOGIN_FAILED';
|
|
58
|
+
send(res, code === 'INVALID_INPUT' ? 400 : 401, { code });
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}));
|
|
62
|
+
ctx.effect(() => webServer.register({
|
|
63
|
+
kind: 'exact',
|
|
64
|
+
path: LOGOUT_PATH,
|
|
65
|
+
handler: async (req, res) => {
|
|
66
|
+
if (req.method !== 'POST') {
|
|
67
|
+
send(res, 405, { code: 'METHOD_NOT_ALLOWED' });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await config.logout();
|
|
72
|
+
send(res, 200, { ok: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
send(res, 400, { code: 'LOGOUT_FAILED' });
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-client-connection 优先看 __DSH_TRANSPORT__:ownsHost:true 即「页面骑在拥有 host 会话的传输上」(由
|
|
3
|
+
* dsh-host-webserver 渲染进 <head> 最前),浏览器端 settings scope 因此 durable;fetch/openStream 故意留空。
|
|
4
|
+
*/
|
|
5
|
+
export function loopbackTransportRow() {
|
|
6
|
+
return { kind: 'global', name: '__DSH_TRANSPORT__', value: { ownsHost: true } };
|
|
7
|
+
}
|
|
8
|
+
export function installLoopbackBridge(ctx) {
|
|
9
|
+
ctx.effect(() => ctx.on('webserver/index-inject', (table) => {
|
|
10
|
+
table.push(loopbackTransportRow());
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { appendFileSync } from 'node:fs';
|
|
2
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
3
|
+
/**
|
|
4
|
+
* 现场诊断用请求 trace:SEMI2_PREVIEW_DEBUG=1 时写 /tmp/semi2-preview-proxy.log。CNB 预览网关夹在
|
|
5
|
+
* 浏览器与本代理之间,它转发的请求形状在容器外不可观测,只有这份 trace 能看到。
|
|
6
|
+
*/
|
|
7
|
+
const debugLog = process.env.SEMI2_PREVIEW_DEBUG === '1'
|
|
8
|
+
? (line) => {
|
|
9
|
+
try {
|
|
10
|
+
appendFileSync('/tmp/semi2-preview-proxy.log', `${new Date().toISOString()} ${line}\n`);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// 仅诊断用:日志失败不得影响服务。
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
: undefined;
|
|
17
|
+
/** 代理面向浏览器/CNB 的连接保持的空闲窗口(ms)。 */
|
|
18
|
+
export const KEEP_ALIVE_TIMEOUT_MS = 72_000;
|
|
19
|
+
/**
|
|
20
|
+
* 把信任边界头改写成固定的环回上游 authority:dsh 的 /api 信任栅栏只接受环回 Host 或预声明 authority,
|
|
21
|
+
* 且会话 cookie 绑定请求 Host(CNB 边缘的 authority 拼写会漂);Origin 一并归一为同源、sec-fetch-site 钉 same-origin。
|
|
22
|
+
*/
|
|
23
|
+
export function rewriteHeaders(headers, upstreamHost) {
|
|
24
|
+
const output = { ...headers, host: upstreamHost };
|
|
25
|
+
if (typeof output.origin === 'string') {
|
|
26
|
+
try {
|
|
27
|
+
output.origin = `${new URL(output.origin).protocol}//${upstreamHost}`;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
output.origin = `http://${upstreamHost}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (output['sec-fetch-site'] !== undefined)
|
|
34
|
+
output['sec-fetch-site'] = 'same-origin';
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 干净 `GET /` 不带有效 cookie 会被 dsh 弹 401(cookie 只在 `?token=` 交换时签发),故代理代用户把裸 `/`
|
|
39
|
+
* 转发成 `/?token=…`(token 不出服务端);已持 cookie 的后续请求绝不能再重放(否则 303 打转),非 index 原样转发。
|
|
40
|
+
*/
|
|
41
|
+
export function indexProxyPath(requestUrl, authenticatedIndexUrl, shouldReplay) {
|
|
42
|
+
if (!shouldReplay)
|
|
43
|
+
return requestUrl;
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(requestUrl, 'http://dsh.invalid');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return requestUrl;
|
|
50
|
+
}
|
|
51
|
+
if (url.pathname !== '/' || url.search !== '' || url.hash !== '')
|
|
52
|
+
return requestUrl;
|
|
53
|
+
const magic = new URL(authenticatedIndexUrl);
|
|
54
|
+
return `${magic.pathname}${magic.search}`;
|
|
55
|
+
}
|
|
56
|
+
/** 安装入口:`ws` 惰性加载——headless 面永不执行到这里,也就永不加载 ws。 */
|
|
57
|
+
export async function installPreviewProxy(ctx, runtime) {
|
|
58
|
+
const { WebSocket, WebSocketServer } = await import('ws');
|
|
59
|
+
const connection = ctx.get('connection');
|
|
60
|
+
const upstreamHost = `127.0.0.1:${runtime.dshPort}`;
|
|
61
|
+
const authenticatedIndexUrl = connection.authenticatedUrl(`http://${upstreamHost}/`);
|
|
62
|
+
/**
|
|
63
|
+
* 代理自持(单租户)会话:CNB 预览边缘服务端跟随重定向却不保留上游 Set-Cookie,签发的 cookie 回不到浏览器,
|
|
64
|
+
* 边缘会在 token 交换 303 上打转——故由代理注入 cookie、吸收 303、对客户端返回最终文档(对外等同 dsh 原行为)。
|
|
65
|
+
*/
|
|
66
|
+
const session = {};
|
|
67
|
+
const requestHasBody = (method) => method === 'POST' || method === 'PUT' || method === 'PATCH';
|
|
68
|
+
const proxy = createServer((request, response) => {
|
|
69
|
+
const requestPath = request.url ?? '/';
|
|
70
|
+
const respond = (attempt, path) => {
|
|
71
|
+
const rewritten = rewriteHeaders(request.headers, upstreamHost);
|
|
72
|
+
if (session.cookie !== undefined)
|
|
73
|
+
rewritten.cookie = session.cookie;
|
|
74
|
+
debugLog?.(`http ${request.method} ${path} host=${String(request.headers.host)} session=${session.cookie !== undefined ? 'yes' : 'no'} attempt=${attempt}`);
|
|
75
|
+
const upstream = httpRequest({
|
|
76
|
+
hostname: '127.0.0.1',
|
|
77
|
+
port: runtime.dshPort,
|
|
78
|
+
path,
|
|
79
|
+
method: request.method,
|
|
80
|
+
headers: rewritten,
|
|
81
|
+
}, (upstreamResponse) => {
|
|
82
|
+
const status = upstreamResponse.statusCode ?? 502;
|
|
83
|
+
const setCookie = (upstreamResponse.headers['set-cookie'] ?? []).map(c => c.split(';')[0]).join('; ');
|
|
84
|
+
debugLog?.(`http ← ${status} ${path} set-cookie=${(upstreamResponse.headers['set-cookie'] ?? []).length}`);
|
|
85
|
+
// 吸收 token 交换 303:采用新 cookie 并用最终文档答同一请求(请求体无法重放,故仅限无体请求)。
|
|
86
|
+
if (status === 303 && setCookie !== '' && attempt < 2 && !requestHasBody(request.method)) {
|
|
87
|
+
session.cookie = session.cookie === undefined ? setCookie : `${session.cookie}; ${setCookie}`;
|
|
88
|
+
upstreamResponse.resume();
|
|
89
|
+
respond(attempt + 1, requestPath);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// 会话过期:丢弃并重做一次 token 交换。
|
|
93
|
+
if (status === 401 && attempt < 2 && !requestHasBody(request.method)) {
|
|
94
|
+
session.cookie = undefined;
|
|
95
|
+
upstreamResponse.resume();
|
|
96
|
+
respond(attempt + 1, indexProxyPath(requestPath, authenticatedIndexUrl, true));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
response.writeHead(status, upstreamResponse.headers);
|
|
100
|
+
upstreamResponse.pipe(response);
|
|
101
|
+
});
|
|
102
|
+
upstream.on('error', (error) => {
|
|
103
|
+
debugLog?.(`http !! upstream error ${request.url}: ${error.message}`);
|
|
104
|
+
if (!response.headersSent) {
|
|
105
|
+
response.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
|
|
106
|
+
response.end(`semi2 preview upstream error: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
request.pipe(upstream);
|
|
110
|
+
};
|
|
111
|
+
respond(0, indexProxyPath(requestPath, authenticatedIndexUrl, session.cookie === undefined));
|
|
112
|
+
});
|
|
113
|
+
/** 经 token 交换铸造会话 cookie(WS 升级前用)。 */
|
|
114
|
+
const ensureSession = () => new Promise((resolve) => {
|
|
115
|
+
if (session.cookie !== undefined) {
|
|
116
|
+
resolve();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const magic = new URL(authenticatedIndexUrl);
|
|
120
|
+
const exchange = httpRequest({
|
|
121
|
+
hostname: '127.0.0.1',
|
|
122
|
+
port: runtime.dshPort,
|
|
123
|
+
path: `${magic.pathname}${magic.search}`,
|
|
124
|
+
method: 'GET',
|
|
125
|
+
headers: { host: upstreamHost },
|
|
126
|
+
}, (res) => {
|
|
127
|
+
const setCookie = (res.headers['set-cookie'] ?? []).map(c => c.split(';')[0]).join('; ');
|
|
128
|
+
if (setCookie !== '')
|
|
129
|
+
session.cookie = setCookie;
|
|
130
|
+
debugLog?.(`session mint ← ${res.statusCode} set-cookie=${(res.headers['set-cookie'] ?? []).length}`);
|
|
131
|
+
res.resume();
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
134
|
+
exchange.on('error', () => { resolve(); });
|
|
135
|
+
exchange.end();
|
|
136
|
+
});
|
|
137
|
+
const sockets = new WebSocketServer({ noServer: true, maxPayload: 256 * 1024 * 1024 });
|
|
138
|
+
proxy.on('upgrade', (request, socket, head) => {
|
|
139
|
+
debugLog?.(`ws upgrade ${request.url} host=${String(request.headers.host)}`);
|
|
140
|
+
void ensureSession().then(() => {
|
|
141
|
+
const headers = rewriteHeaders(request.headers, upstreamHost);
|
|
142
|
+
if (session.cookie !== undefined)
|
|
143
|
+
headers.cookie = session.cookie;
|
|
144
|
+
const upstream = new WebSocket(`ws://${upstreamHost}${request.url ?? '/'}`, {
|
|
145
|
+
headers,
|
|
146
|
+
perMessageDeflate: false,
|
|
147
|
+
});
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
upstream.terminate();
|
|
150
|
+
socket.destroy();
|
|
151
|
+
}, 10_000);
|
|
152
|
+
upstream.once('open', () => {
|
|
153
|
+
debugLog?.(`ws open ${request.url}`);
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
sockets.handleUpgrade(request, socket, head, (client) => {
|
|
156
|
+
const pingDown = setInterval(() => {
|
|
157
|
+
if (client.readyState === WebSocket.OPEN)
|
|
158
|
+
client.ping();
|
|
159
|
+
}, runtime.pingIntervalMs);
|
|
160
|
+
const pingUp = setInterval(() => {
|
|
161
|
+
if (upstream.readyState === WebSocket.OPEN)
|
|
162
|
+
upstream.ping();
|
|
163
|
+
}, runtime.pingIntervalMs);
|
|
164
|
+
const close = () => {
|
|
165
|
+
clearInterval(pingDown);
|
|
166
|
+
clearInterval(pingUp);
|
|
167
|
+
client.terminate();
|
|
168
|
+
upstream.terminate();
|
|
169
|
+
};
|
|
170
|
+
client.on('message', (data, binary) => {
|
|
171
|
+
if (upstream.readyState === WebSocket.OPEN)
|
|
172
|
+
upstream.send(data, { binary });
|
|
173
|
+
});
|
|
174
|
+
upstream.on('message', (data, binary) => {
|
|
175
|
+
if (client.readyState === WebSocket.OPEN)
|
|
176
|
+
client.send(data, { binary });
|
|
177
|
+
});
|
|
178
|
+
client.once('close', close);
|
|
179
|
+
upstream.once('close', close);
|
|
180
|
+
client.once('error', close);
|
|
181
|
+
upstream.once('error', close);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
upstream.once('error', () => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
socket.destroy();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// Node 默认 5s keep-alive 会在 CNB 反代的复用窗口之前关掉空闲连接,复用半关 socket 会回 RST
|
|
191
|
+
// (浏览器报 ERR_CONNECTION_RESET);keepAliveTimeout 必须高于下游复用窗口,headersTimeout 再高 3s。
|
|
192
|
+
proxy.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
|
|
193
|
+
proxy.headersTimeout = KEEP_ALIVE_TIMEOUT_MS + 3_000;
|
|
194
|
+
proxy.listen(runtime.previewPort, '0.0.0.0');
|
|
195
|
+
ctx.effect(() => () => {
|
|
196
|
+
for (const client of sockets.clients)
|
|
197
|
+
client.terminate();
|
|
198
|
+
sockets.close();
|
|
199
|
+
proxy.close();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
/** 预览反代监听端口(CNB 预览网关与 WebIDE 侧栏都按 8788 约定)。 */
|
|
4
|
+
const PREVIEW_PORT = 8788;
|
|
5
|
+
/** WebSocket 保活间隔。 */
|
|
6
|
+
const PING_INTERVAL_MS = 5_000;
|
|
7
|
+
/** cwd 对齐 /workspace 并同步 HOME(dsh 新会话默认目录取 homedir);dshPort 取官方 webServer 监听端口。 */
|
|
8
|
+
export function createWorkstationRuntime(dshPort) {
|
|
9
|
+
const hasWorkspace = existsSync('/workspace');
|
|
10
|
+
const cwd = resolve(hasWorkspace ? '/workspace' : process.cwd());
|
|
11
|
+
if (hasWorkspace)
|
|
12
|
+
process.env.HOME = cwd;
|
|
13
|
+
return {
|
|
14
|
+
cwd,
|
|
15
|
+
dshPort,
|
|
16
|
+
previewPort: PREVIEW_PORT,
|
|
17
|
+
pingIntervalMs: PING_INTERVAL_MS,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-client-ui-settings-models 中 bundle 私有常量的镜像(比 notice 文案高一层):升级 pinned dsh channel
|
|
3
|
+
* (DSH_VERSION)时须核对该串仍等于客户端 bundle 里的值,回归测试钉住它;陈旧值只退化为原生行为。
|
|
4
|
+
*/
|
|
5
|
+
export const WELCOME_NOTICE_VERSION = '2026-08-13.1';
|
|
6
|
+
/** 产品级 GUI onboarding 事实的 durable settings 命名空间。 */
|
|
7
|
+
export const WELCOME_NOTICE_NAMESPACE = 'ui-onboarding';
|
|
8
|
+
/** 记录已确认的欢迎声明文案版本。 */
|
|
9
|
+
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion';
|
|
10
|
+
export const SEED_INTERVAL_MS = 50;
|
|
11
|
+
export const SEED_ATTEMPTS = 40;
|
|
12
|
+
function defaultWait(ms) {
|
|
13
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
/** 等 "ui-onboarding" 注册后并入当前版本号(幂等);拒绝/失败会 reject 交调用方降级,命名空间始终未就绪返回 false。 */
|
|
16
|
+
export async function seedWelcomeAcknowledgement(settings, options = {}) {
|
|
17
|
+
const namespace = options.namespace ?? WELCOME_NOTICE_NAMESPACE;
|
|
18
|
+
const intervalMs = options.intervalMs ?? SEED_INTERVAL_MS;
|
|
19
|
+
const attempts = options.attempts ?? SEED_ATTEMPTS;
|
|
20
|
+
const wait = options.wait ?? defaultWait;
|
|
21
|
+
const patch = { [WELCOME_NOTICE_ACK_FIELD]: WELCOME_NOTICE_VERSION };
|
|
22
|
+
for (let remaining = attempts;; remaining -= 1) {
|
|
23
|
+
if (settings.describe().some(({ ns }) => ns === namespace)) {
|
|
24
|
+
await settings.update(namespace, patch);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (remaining <= 0)
|
|
28
|
+
return false;
|
|
29
|
+
await wait(intervalMs);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function warn(ctx, message) {
|
|
33
|
+
try {
|
|
34
|
+
ctx.logger('semi2-welcome-ack').warn(message);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
/** 在可选 `settings` 服务上 best-effort 预置:只降级。 */
|
|
39
|
+
export function installWelcomeAck(ctx) {
|
|
40
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
41
|
+
const settings = settingsCtx.get('settings');
|
|
42
|
+
if (settings === undefined)
|
|
43
|
+
return;
|
|
44
|
+
void seedWelcomeAcknowledgement(settings).then((seeded) => {
|
|
45
|
+
if (!seeded)
|
|
46
|
+
warn(ctx, 'ui-onboarding 命名空间未就绪,跳过欢迎声明预置');
|
|
47
|
+
}).catch((error) => {
|
|
48
|
+
warn(ctx, '写入 ui-onboarding 欢迎声明确认失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fa-software/semi2-dsh",
|
|
3
|
+
"version": "0.0.10-preview.20260920042705",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Semi2 单插件:semi2-hub 配置引导 + codegraph 结构索引 + 公共 Web 能力。",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"cordis.patch.yml"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"dsh": {
|
|
26
|
+
"bundle": {
|
|
27
|
+
"patch": "./cordis.patch.yml"
|
|
28
|
+
},
|
|
29
|
+
"client": {
|
|
30
|
+
"platform": "web",
|
|
31
|
+
"inject": [
|
|
32
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
33
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@colbymchenry/codegraph": "1.6.0",
|
|
39
|
+
"ws": "^8.21.3"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
+
"@deepseek-ai/dsh-agent": "^0.1.6-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-credentials": "^0.1.6-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.6-alpha.2",
|
|
46
|
+
"@deepseek-ai/dsh-llm": "^0.1.6-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-skill": "^0.1.6-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.6-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.6-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-tools": "^0.1.6-alpha.2",
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@deepseek-ai/dsh-credentials": {
|
|
55
|
+
"optional": true
|
|
56
|
+
},
|
|
57
|
+
"@deepseek-ai/dsh-host-webserver": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@deepseek-ai/dsh-util-values": "^0.1.6-alpha.2",
|
|
63
|
+
"@graphql-codegen/cli": "7.4.0",
|
|
64
|
+
"@graphql-codegen/typescript": "6.1.0",
|
|
65
|
+
"@graphql-codegen/typescript-operations": "6.1.6",
|
|
66
|
+
"@types/node": "^24.0.0",
|
|
67
|
+
"@types/react": "^19.0.0",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
69
|
+
"graphql": "^16.11.0",
|
|
70
|
+
"react": "^19.2.0",
|
|
71
|
+
"tsdown": "^0.22.2",
|
|
72
|
+
"typescript": "~5.8.3"
|
|
73
|
+
},
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "rm -rf lib && graphql-codegen -c codegen.ts && tsc -p tsconfig.json && tsdown -c tsdown.config.ts",
|
|
76
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
77
|
+
"test": "pnpm run build && node --test test/*.test.mjs",
|
|
78
|
+
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
79
|
+
"dev:client": "tsdown -c tsdown.config.ts --watch"
|
|
80
|
+
}
|
|
81
|
+
}
|