@rezti/dsh-rez-wechat 0.1.17 → 0.1.19

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.md CHANGED
@@ -6,7 +6,7 @@
6
6
  dsh plugin --profile web add @rezti/dsh-rez-suite
7
7
  ```
8
8
 
9
- 以后升级只记:`dsh plugin --profile web update @rezti/dsh-rez-suite`。不要再单独 add 本包。
9
+ 以后升级只记:`dsh plugin --profile web add @rezti/dsh-rez-suite@latest`。不要再单独 add 本包。
10
10
 
11
11
  | 能力 | 做法 |
12
12
  |---|---|
package/lib/web-shim.d.ts CHANGED
@@ -9,6 +9,14 @@
9
9
  */
10
10
  export declare const WECHAT_WORKSPACE_NAME = "\u5FAE\u4FE1";
11
11
  export declare const DEFAULT_WEB_URL = "http://127.0.0.1:3080";
12
+ /** Written by suite when dsh web boots; carries ?token= for 0.1.5 browser auth. */
13
+ export declare function dshWebUrlFile(env?: NodeJS.ProcessEnv, home?: string): string;
14
+ /** Origin only (no path/query). API posts go to `${origin}/api/...`. */
15
+ export declare function webBaseUrl(env?: NodeJS.ProcessEnv, home?: string): string;
16
+ /** Launch token from env, REZ_DSH_WEB_URL query, or ~/.dsh/dsh-web.url. */
17
+ export declare function webLaunchToken(env?: NodeJS.ProcessEnv, home?: string): string | undefined;
18
+ /** Exchange launch token for the HttpOnly dsh-auth cookie (dsh 0.1.5+). */
19
+ export declare function mintWebAuthCookie(origin: string, token: string, timeoutMs?: number): Promise<string>;
12
20
  /** Seeded only when the inbox folder has no AGENTS.md yet. */
13
21
  export declare const WECHAT_INBOX_AGENTS: string;
14
22
  export interface ModelRef {
@@ -65,7 +73,6 @@ export declare function wechatWorkspaceDir(env?: NodeJS.ProcessEnv, home?: strin
65
73
  /** Write a short inbox AGENTS.md once; never overwrite a living file. Skip non-微信 rooms (企微按房间绑定). */
66
74
  export declare function ensureWechatInboxAgents(cwd: string): void;
67
75
  export declare function sessionStorePath(env?: NodeJS.ProcessEnv, home?: string): string;
68
- export declare function webBaseUrl(env?: NodeJS.ProcessEnv): string;
69
76
  export declare function loadSessionStore(path: string): SessionStore;
70
77
  export declare function saveSessionStore(path: string, store: SessionStore): void;
71
78
  export declare function encodeClientRequest(method: string, payload: Record<string, unknown>, rpcId?: string): {
@@ -104,7 +111,7 @@ export declare function weixinBridgeEnv(opts: {
104
111
  launcherPath: string;
105
112
  shimScript: string;
106
113
  }): NodeJS.ProcessEnv;
107
- export declare function postRpc(baseUrl: string, method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
114
+ export declare function postRpc(baseUrl: string, method: string, params: Record<string, unknown>, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<unknown>;
108
115
  export declare function extractArchivedSessionIds(body: unknown): string[];
109
116
  /** Stock dsh archives one-way and hides blank sessions. Restore + attach + title. */
110
117
  export declare function ensureSessionVisible(rpc: RpcFn, sessionId: string, env?: NodeJS.ProcessEnv, workspaceId?: string, loopback?: boolean): Promise<void>;
package/lib/web-shim.js CHANGED
@@ -13,6 +13,107 @@ import { homedir } from 'node:os';
13
13
  import { basename, dirname, join } from 'node:path';
14
14
  export const WECHAT_WORKSPACE_NAME = '微信';
15
15
  export const DEFAULT_WEB_URL = 'http://127.0.0.1:3080';
16
+ /** Written by suite when dsh web boots; carries ?token= for 0.1.5 browser auth. */
17
+ export function dshWebUrlFile(env = process.env, home = homedir()) {
18
+ return join(dshHomeDir(env, home), 'dsh-web.url');
19
+ }
20
+ /** Origin only (no path/query). API posts go to `${origin}/api/...`. */
21
+ export function webBaseUrl(env = process.env, home = homedir()) {
22
+ const fromFile = (() => {
23
+ try {
24
+ return readFileSync(dshWebUrlFile(env, home), 'utf8').trim();
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ })();
30
+ for (const raw of [env.REZ_DSH_WEB_URL, fromFile, DEFAULT_WEB_URL]) {
31
+ if (raw === undefined || raw.trim() === '')
32
+ continue;
33
+ try {
34
+ const withScheme = /:\/\//.test(raw.trim()) ? raw.trim() : `http://${raw.trim()}`;
35
+ const url = new URL(withScheme);
36
+ return `${url.protocol}//${url.host}`;
37
+ }
38
+ catch {
39
+ /* try next */
40
+ }
41
+ }
42
+ return DEFAULT_WEB_URL;
43
+ }
44
+ /** Launch token from env, REZ_DSH_WEB_URL query, or ~/.dsh/dsh-web.url. */
45
+ export function webLaunchToken(env = process.env, home = homedir()) {
46
+ const fromEnv = env.REZ_DSH_WEB_TOKEN?.trim();
47
+ if (fromEnv)
48
+ return fromEnv;
49
+ const candidates = [env.REZ_DSH_WEB_URL, (() => {
50
+ try {
51
+ return readFileSync(dshWebUrlFile(env, home), 'utf8').trim();
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ })()];
57
+ for (const raw of candidates) {
58
+ if (raw === undefined || raw === '')
59
+ continue;
60
+ try {
61
+ const withScheme = /:\/\//.test(raw) ? raw : `http://${raw}`;
62
+ const token = new URL(withScheme).searchParams.get('token');
63
+ if (token !== null && token.length > 0)
64
+ return token;
65
+ }
66
+ catch {
67
+ /* try next */
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ const authCookies = new Map();
73
+ function collectSetCookies(headers) {
74
+ const anyHeaders = headers;
75
+ if (typeof anyHeaders.getSetCookie === 'function') {
76
+ return anyHeaders.getSetCookie();
77
+ }
78
+ const single = headers.get('set-cookie');
79
+ return single === null ? [] : [single];
80
+ }
81
+ /** Exchange launch token for the HttpOnly dsh-auth cookie (dsh 0.1.5+). */
82
+ export async function mintWebAuthCookie(origin, token, timeoutMs = RPC_TIMEOUT_MS) {
83
+ const url = `${origin.replace(/\/$/, '')}/?token=${encodeURIComponent(token)}`;
84
+ let response;
85
+ try {
86
+ response = await fetch(url, {
87
+ method: 'GET',
88
+ redirect: 'manual',
89
+ signal: AbortSignal.timeout(timeoutMs),
90
+ });
91
+ }
92
+ catch (error) {
93
+ const why = error instanceof Error ? error.message : String(error);
94
+ throw new Error(`换取 dsh web 认证 cookie 失败(${origin}):${why}`);
95
+ }
96
+ const pairs = collectSetCookies(response.headers)
97
+ .map((row) => row.split(';', 1)[0]?.trim())
98
+ .filter((row) => typeof row === 'string' && row.includes('='));
99
+ if (pairs.length === 0) {
100
+ throw new Error('dsh web 未下发认证 cookie。请用终端打印的完整地址打开网页(必须带 ?token=),或确认 ~/.dsh/dsh-web.url 已由 suite 写入。');
101
+ }
102
+ return pairs.join('; ');
103
+ }
104
+ async function authCookieFor(origin, env, force = false) {
105
+ if (!force) {
106
+ const cached = authCookies.get(origin);
107
+ if (cached !== undefined)
108
+ return cached;
109
+ }
110
+ const token = webLaunchToken(env);
111
+ if (token === undefined)
112
+ return undefined;
113
+ const cookie = await mintWebAuthCookie(origin, token);
114
+ authCookies.set(origin, cookie);
115
+ return cookie;
116
+ }
16
117
  /** Seeded only when the inbox folder has no AGENTS.md yet. */
17
118
  export const WECHAT_INBOX_AGENTS = [
18
119
  '# 微信',
@@ -151,9 +252,6 @@ export function sessionStorePath(env = process.env, home = homedir()) {
151
252
  return env.REZ_WECHAT_SESSION_STORE;
152
253
  return join(dshHomeDir(env, home), 'dsh-rez-weixin', 'web-sessions.json');
153
254
  }
154
- export function webBaseUrl(env = process.env) {
155
- return (env.REZ_DSH_WEB_URL ?? DEFAULT_WEB_URL).replace(/\/$/, '');
156
- }
157
255
  export function loadSessionStore(path) {
158
256
  try {
159
257
  const parsed = JSON.parse(readFileSync(path, 'utf8'));
@@ -717,27 +815,60 @@ export function weixinBridgeEnv(opts) {
717
815
  env.DSH_BRIDGE_DSH = opts.launcherPath;
718
816
  env.REZ_WECHAT_SESSION_STORE = env.REZ_WECHAT_SESSION_STORE ?? join(opts.dataDir, 'web-sessions.json');
719
817
  env.REZ_WECHAT_WORKSPACE = env.REZ_WECHAT_WORKSPACE ?? wechatWorkspaceDir(env);
720
- env.REZ_DSH_WEB_URL = env.REZ_DSH_WEB_URL ?? DEFAULT_WEB_URL;
818
+ // Prefer printed/suite URL (port + ?token=); do not force bare :3080 over it.
819
+ if (env.REZ_DSH_WEB_URL === undefined || env.REZ_DSH_WEB_URL.trim() === '') {
820
+ try {
821
+ const printed = readFileSync(dshWebUrlFile(env), 'utf8').trim();
822
+ if (printed.length > 0)
823
+ env.REZ_DSH_WEB_URL = printed;
824
+ }
825
+ catch {
826
+ env.REZ_DSH_WEB_URL = DEFAULT_WEB_URL;
827
+ }
828
+ }
721
829
  return env;
722
830
  }
723
831
  async function sleep(ms) {
724
832
  await new Promise(resolve => setTimeout(resolve, ms));
725
833
  }
726
- export async function postRpc(baseUrl, method, params, timeoutMs = RPC_TIMEOUT_MS) {
727
- const url = `${baseUrl.replace(/\/$/, '')}/api/${method}`;
834
+ export async function postRpc(baseUrl, method, params, timeoutMs = RPC_TIMEOUT_MS, env = process.env) {
835
+ const origin = (() => {
836
+ try {
837
+ const withScheme = /:\/\//.test(baseUrl) ? baseUrl : `http://${baseUrl}`;
838
+ const url = new URL(withScheme);
839
+ return `${url.protocol}//${url.host}`;
840
+ }
841
+ catch {
842
+ return webBaseUrl(env);
843
+ }
844
+ })();
845
+ const url = `${origin}/api/${method}`;
728
846
  const envelope = encodeClientRequest(method, params);
729
- let response;
730
- try {
731
- response = await fetch(url, {
847
+ const send = async (cookie) => {
848
+ const headers = { 'content-type': 'application/json' };
849
+ if (cookie !== undefined && cookie.length > 0)
850
+ headers.cookie = cookie;
851
+ return fetch(url, {
732
852
  method: 'POST',
733
- headers: { 'content-type': 'application/json' },
853
+ headers,
734
854
  body: JSON.stringify(envelope),
735
855
  signal: AbortSignal.timeout(timeoutMs),
736
856
  });
857
+ };
858
+ let cookie = await authCookieFor(origin, env);
859
+ let response;
860
+ try {
861
+ response = await send(cookie);
862
+ if (response.status === 401) {
863
+ authCookies.delete(origin);
864
+ cookie = await authCookieFor(origin, env, true);
865
+ if (cookie !== undefined)
866
+ response = await send(cookie);
867
+ }
737
868
  }
738
869
  catch (error) {
739
870
  const why = error instanceof Error ? error.message : String(error);
740
- throw new Error(`连不上本机 dsh web(${baseUrl}):${why}。请保持网页端开着。`);
871
+ throw new Error(`连不上本机 dsh web(${origin}):${why}。请保持网页端开着。`);
741
872
  }
742
873
  const raw = await response.text();
743
874
  let parsed;
@@ -745,6 +876,9 @@ export async function postRpc(baseUrl, method, params, timeoutMs = RPC_TIMEOUT_M
745
876
  parsed = raw === '' ? {} : JSON.parse(raw);
746
877
  }
747
878
  catch {
879
+ if (response.status === 401) {
880
+ throw new Error(`dsh web ${method} 返回 401 unauthorized。请用终端打印的带 ?token= 的完整 URL 打开网页一次,或升级 suite 后确认 ~/.dsh/dsh-web.url 存在。原文: ${raw.slice(0, 160)}`);
881
+ }
748
882
  throw new Error(`dsh web ${method} 返回非 JSON (${response.status}): ${raw.slice(0, 240)}`);
749
883
  }
750
884
  const body = unwrapRpc(parsed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rezti/dsh-rez-wechat",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "ReZ-TI WeChat/WeCom bridges. Personal WeChat is QClaw/ClawBot scan-and-chat via dsh-wechat-bridge; WeCom AI bots use the official @wecom/aibot-node-sdk (BotID + Secret) bound per Harness room.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,14 +27,14 @@
27
27
  ],
28
28
  "license": "Apache-2.0",
29
29
  "dependencies": {
30
+ "@rezti/dsh-rez-sso": "^0.1.18",
30
31
  "@wecom/aibot-node-sdk": "^1.0.7",
31
- "qrcode": "^1.5.4",
32
- "@rezti/dsh-rez-sso": "0.1.14"
32
+ "qrcode": "^1.5.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
37
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
36
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
37
+ "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
38
38
  "@types/node": "^22.20.0",
39
39
  "@types/qrcode": "^1.5.5",
40
40
  "typescript": "~5.7.2"