@77-zhike/app-sdk 0.2.0-d7b1.12 → 0.2.0-d7b1.14

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
@@ -1,5 +1,13 @@
1
1
  # 智客 App SDK 与 CLI
2
2
 
3
+ ## 操作系统与 Windows 候选
4
+
5
+ 当前 npm 发布版的 Developer CLI 仅提供 macOS Keychain 适配。本开发分支新增 Windows Credential Manager 候选:复用现有浏览器 PKCE 和 Session,使用系统 Windows PowerShell 5.1 固定桥接,refresh credential 按当前用户和 API origin 存储。
6
+
7
+ Windows 候选通过独立的 `SDK Windows 自动化验证(不发布)` Actions 检查,分别运行 Node 22/24。测试制品仅供验证,未发布 npm;Windows 11 桌面、系统默认浏览器弹起、双用户和注销重登暂未覆盖。完整结果与边界见 `openspec/changes/windows-app-developer-cli/`。
8
+
9
+ `zhike doctor --json` 的 `secureStore` 检查表示当前机制可用性,`writeAccess: not_checked` 和 `authentication: not_checked` 不证明凭据已保存、登录有效或业务可用。系统策略禁止桥接时会返回安全存储错误,不修改系统策略或降级为明文文件。
10
+
3
11
  ## 获准依赖
4
12
 
5
13
  工具链从 `src/toolchain/allowed-packages.json` 读取平台维护的包白名单。目前允许 `@77-zhike/qiqi-openapi@0.1.0` 的服务端包根入口 `.`;前端不可导入。项目安装依赖后直接使用静态 ESM import,真实解析的包名、精确版本、入口及间接模块在打包时核对,代码合并进 Artifact。App 的 Manifest、Settings 或 `package.json` 不能扩展名单。
@@ -253,3 +261,11 @@ Chart 仅接受纯数据 `labels` 与 `series`,不接受脚本、HTML 或任
253
261
  ### 精确数字的表格录入
254
262
 
255
263
  `form.TextInput` 可选 `labelHidden` 和 `inputMode="numeric" | "decimal"`。隐藏标题仍保留与控件关联的读屏标签,默认标题继续显示,字段错误和说明仍显示。`inputMode` 只提示键盘,不转换、过滤或格式化值,不替代业务校验;需要保留十进制字符串或未完成输入时使用,数值型字段继续使用 `NumberInput`。
264
+
265
+ ## App 对话输入部件
266
+
267
+ `Conversation.Root` 内自由组合 Messages、History、NewButton、Status、Composer。History 默认使用与 automation 相同的图标弹出列表;Composer 默认带附件/图片上传和语音回填,转写后仍需用户发送。输入能力由 Host 提供,不把 File/音频字节或凭据交给 App Worker。
268
+
269
+ 附件只读当前 App 会话中用户提交的文件,不扩展自定义 Tool 的权限。新 App 业务 Tool 仍在 agent 目录用 `.tools.ts` 合并定义并由 Integration Runtime 执行。元数据 Schema 必须为静态字面量,execute 可导入项目自己的业务校验函数。
270
+
271
+ 真实表单协作可参考 `built-in-apps/opportunity-quotes`:页面以 SDK 布局并列 Form 与 Conversation,callback 接收建议,用户确认后更新本地草稿;正式保存沿原入口。NativeDOM 内仍只允许原有控件,不直接放 Conversation;可以在 Conversation 内用独立 NativeDOM 展示本地建议确认区。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@77-zhike/app-sdk",
3
- "version": "0.2.0-d7b1.12",
3
+ "version": "0.2.0-d7b1.14",
4
4
  "description": "智客 Integration App 一期 SDK 与本地 Toolchain",
5
5
  "repository": {
6
6
  "type": "git",
@@ -67,5 +67,5 @@
67
67
  "optional": true
68
68
  }
69
69
  },
70
- "gitHead": "371b8a58d0f56deb5d1a330a688bc84c90704231"
70
+ "gitHead": "04ae9c07742d99322d12790db9c7934cea8f6f64"
71
71
  }
@@ -2,6 +2,7 @@ import { execFile as execFileCallback } from "node:child_process";
2
2
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
3
3
  import { createServer } from "node:http";
4
4
  import { promisify } from "node:util";
5
+ import { win32 } from "node:path";
5
6
 
6
7
  import { CLI_CLIENT_ID } from "../config.js";
7
8
  import { ZhikeCliError } from "../errors.js";
@@ -17,25 +18,33 @@ export async function authorizeCli({
17
18
  openBrowser = openSystemBrowser,
18
19
  onProgress = () => {},
19
20
  timeoutMs = 120_000,
21
+ signal,
20
22
  }) {
21
23
  const state = randomBytes(32).toString("base64url");
22
24
  const codeVerifier = randomBytes(48).toString("base64url");
23
25
  const codeChallenge = createHash("sha256")
24
26
  .update(codeVerifier, "ascii")
25
27
  .digest("base64url");
26
- const callback = await createLoopbackCallback({ state, timeoutMs });
27
- const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
28
- const authorizationUrl = new URL("/cli/authorize", consoleBaseUrl);
29
- authorizationUrl.searchParams.set("client_id", CLI_CLIENT_ID);
30
- authorizationUrl.searchParams.set("redirect_uri", redirectUri);
31
- authorizationUrl.searchParams.set("code_challenge", codeChallenge);
32
- authorizationUrl.searchParams.set("code_challenge_method", "S256");
33
- authorizationUrl.searchParams.set("state", state);
34
-
28
+ const cancellation = new AbortController();
29
+ const stop = () => cancellation.abort();
30
+ const authorizationSignal = signal || cancellation.signal;
31
+ if (!signal) { process.once("SIGINT", stop); process.once("SIGTERM", stop); }
32
+ let callback;
35
33
  try {
34
+ if (authorizationSignal.aborted) throw new ZhikeCliError("zhike_cli_authorization_cancelled", "Developer CLI 授权已取消", { exitCode: 130 });
35
+ callback = await createLoopbackCallback({ state, timeoutMs, signal: authorizationSignal });
36
+ if (authorizationSignal.aborted) await callback.waitForCode;
37
+ const redirectUri = `http://127.0.0.1:${callback.port}/callback`;
38
+ const authorizationUrl = new URL("/cli/authorize", consoleBaseUrl);
39
+ authorizationUrl.searchParams.set("client_id", CLI_CLIENT_ID);
40
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
41
+ authorizationUrl.searchParams.set("code_challenge", codeChallenge);
42
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
43
+ authorizationUrl.searchParams.set("state", state);
36
44
  callback.waitForCode.catch(() => {});
37
45
  onProgress("正在浏览器中完成 Developer CLI 授权...");
38
- await openBrowser(authorizationUrl.toString());
46
+ try { await openBrowser(authorizationUrl.toString()); }
47
+ catch { onProgress(`无法自动打开浏览器,请手动访问:${authorizationUrl}`); }
39
48
  const code = await callback.waitForCode;
40
49
  onProgress("已收到浏览器授权,正在完成 Developer CLI 登录...");
41
50
  const api = new DeveloperApiClient({ apiBaseUrl, fetchImpl });
@@ -45,10 +54,11 @@ export async function authorizeCli({
45
54
  redirect_uri: redirectUri,
46
55
  code_verifier: codeVerifier,
47
56
  });
48
- await credentialStore.set(apiBaseUrl, token.refresh_credential);
57
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
49
58
  return renewableClient(api, token.access_token, credentialStore);
50
59
  } finally {
51
- await callback.close();
60
+ await callback?.close();
61
+ if (!signal) { process.removeListener("SIGINT", stop); process.removeListener("SIGTERM", stop); }
52
62
  }
53
63
  }
54
64
 
@@ -77,7 +87,7 @@ export async function authenticateCli({
77
87
  }
78
88
  if (token) {
79
89
  if (token.refresh_credential !== refreshCredential) {
80
- await credentialStore.set(apiBaseUrl, token.refresh_credential);
90
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
81
91
  }
82
92
  return renewableClient(api, token.access_token, credentialStore);
83
93
  }
@@ -98,16 +108,27 @@ export async function authenticateCli({
98
108
  });
99
109
  }
100
110
 
101
- export async function openSystemBrowser(url, platform = process.platform) {
111
+ export async function openSystemBrowser(url, platform = process.platform, { runner = execFile, systemRoot = process.env.SystemRoot } = {}) {
112
+ const parsed = new URL(url);
113
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password) {
114
+ throw new ZhikeCliError("zhike_browser_open_failed", "浏览器地址必须是无凭据的 HTTP(S) URL");
115
+ }
116
+ // URL 只作为 base64 数据传入固定表达式,避免 cmd/start 把 query 中的 & 当成命令。
117
+ const encodedUrl = Buffer.from(parsed.href, "utf8").toString("base64");
118
+ const windowsCommand = `Start-Process -FilePath ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedUrl}')))`;
102
119
  const command =
103
120
  platform === "darwin"
104
121
  ? ["open", [url]]
105
122
  : platform === "win32"
106
- ? ["cmd", ["/c", "start", "", url]]
123
+ ? [systemRoot && win32.isAbsolute(systemRoot)
124
+ ? win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "",
125
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(windowsCommand, "utf16le").toString("base64")]]
107
126
  : ["xdg-open", [url]];
108
127
  try {
109
- await execFile(command[0], command[1], {
128
+ if (!command[0]) throw new Error("system browser unavailable");
129
+ await runner(command[0], command[1], {
110
130
  windowsHide: true,
131
+ timeout: 10_000,
111
132
  });
112
133
  } catch (error) {
113
134
  throw new ZhikeCliError(
@@ -127,14 +148,30 @@ function renewableClient(api, accessToken, credentialStore) {
127
148
  }
128
149
  const token = await api.refreshCliSession(refreshCredential);
129
150
  if (token.refresh_credential !== refreshCredential) {
130
- await credentialStore.set(api.apiBaseUrl, token.refresh_credential);
151
+ await persistRefreshCredential(api, token.refresh_credential, credentialStore);
131
152
  }
132
153
  return token.access_token;
133
154
  };
134
155
  return client;
135
156
  }
136
157
 
137
- async function createLoopbackCallback({ state, timeoutMs }) {
158
+ async function persistRefreshCredential(api, refreshCredential, credentialStore) {
159
+ try { await credentialStore.set(api.apiBaseUrl, refreshCredential); }
160
+ catch {
161
+ let cleanupConfirmed = false;
162
+ const previousSignal = api.signal;
163
+ try {
164
+ api.signal = AbortSignal.timeout(5_000);
165
+ await api.logoutCliSession(refreshCredential);
166
+ cleanupConfirmed = true;
167
+ } catch { /* 网络失败不改变本地存储失败这一事实。 */ }
168
+ finally { api.signal = previousSignal; }
169
+ throw new ZhikeCliError("zhike_secure_store_failed", "无法保存 Developer CLI 登录凭据,请修复系统安全存储后重新登录"
170
+ + (cleanupConfirmed ? "" : ";远端会话清理未确认"));
171
+ }
172
+ }
173
+
174
+ async function createLoopbackCallback({ state, timeoutMs, signal }) {
138
175
  let timer;
139
176
  let resolveCode;
140
177
  let rejectCode;
@@ -142,6 +179,7 @@ async function createLoopbackCallback({ state, timeoutMs }) {
142
179
  resolveCode = resolve;
143
180
  rejectCode = reject;
144
181
  });
182
+ waitForCode.catch(() => {});
145
183
  const server = createServer((request, response) => {
146
184
  const url = new URL(request.url || "/", "http://127.0.0.1");
147
185
  const receivedState = url.searchParams.get("state") || "";
@@ -169,6 +207,13 @@ async function createLoopbackCallback({ state, timeoutMs }) {
169
207
  server.once("error", reject);
170
208
  server.listen(0, "127.0.0.1", resolve);
171
209
  });
210
+ const cancel = () => {
211
+ rejectCode(new ZhikeCliError("zhike_cli_authorization_cancelled", "Developer CLI 授权已取消", { exitCode: 130 }));
212
+ server.close();
213
+ server.closeAllConnections();
214
+ };
215
+ signal?.addEventListener("abort", cancel, { once: true });
216
+ if (signal?.aborted) cancel();
172
217
  const address = server.address();
173
218
  if (!address || typeof address === "string") {
174
219
  server.close();
@@ -190,6 +235,7 @@ async function createLoopbackCallback({ state, timeoutMs }) {
190
235
  waitForCode,
191
236
  close: async () => {
192
237
  clearTimeout(timer);
238
+ signal?.removeEventListener("abort", cancel);
193
239
  if (!server.listening) {
194
240
  return;
195
241
  }
@@ -1,7 +1,9 @@
1
1
  import { execFile as execFileCallback } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
+ import { randomUUID } from "node:crypto";
3
4
 
4
5
  import { ZhikeCliError } from "../errors.js";
6
+ import { WindowsCredentialStore } from "./windows-credential-store.js";
5
7
 
6
8
  const execFile = promisify(execFileCallback);
7
9
  const KEYCHAIN_SERVICE = "com.77hub.zhike.developer-cli";
@@ -86,12 +88,31 @@ export function createSystemCredentialStore(platform = process.platform) {
86
88
  if (platform === "darwin") {
87
89
  return new MacOSKeychainCredentialStore();
88
90
  }
91
+ if (platform === "win32") {
92
+ return new WindowsCredentialStore();
93
+ }
89
94
  throw new ZhikeCliError(
90
95
  "zhike_secure_store_unavailable",
91
96
  "当前操作系统尚未配置安全凭据存储适配器",
92
97
  );
93
98
  }
94
99
 
100
+ export async function inspectSystemCredentialStore(platform = process.platform, createStore = createSystemCredentialStore) {
101
+ const provider = platform === "darwin" ? "macos-keychain" : platform === "win32" ? "windows-credential-manager" : null;
102
+ const check = { name: "secureStore", platform, provider, supported: Boolean(provider),
103
+ available: false, ok: false, writeAccess: "not_checked" };
104
+ if (!provider) return { ...check, code: "zhike_secure_store_unavailable" };
105
+ try {
106
+ const store = createStore(platform);
107
+ if (store.probe) await store.probe();
108
+ else await store.get(`https://probe-${randomUUID()}.invalid`);
109
+ return { ...check, available: true, ok: true };
110
+ } catch (error) {
111
+ return { ...check, code: ["zhike_secure_store_unavailable", "zhike_secure_store_failed"].includes(error?.code)
112
+ ? error.code : "zhike_secure_store_failed" };
113
+ }
114
+ }
115
+
95
116
  function _keychainError(error) {
96
117
  return new ZhikeCliError(
97
118
  "zhike_secure_store_failed",
@@ -5,6 +5,7 @@ import { resolve } from "node:path";
5
5
  import { loadCliConfig } from "../config.js";
6
6
  import { ZhikeCliError } from "../errors.js";
7
7
  import { readProjectBinding } from "../project-binding.js";
8
+ import { inspectSystemCredentialStore } from "./credential-store.js";
8
9
 
9
10
  const metadata = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
10
11
  export const commandHelp = {
@@ -72,7 +73,7 @@ export async function runLocalCommand(command, args, { cwd, environment }) {
72
73
  } catch {
73
74
  checks.push({ name: "package", ok: false, code: "package_missing_or_invalid" });
74
75
  }
75
- checks.push({ name: "secureStore", ok: process.platform === "darwin", platform: process.platform });
76
+ checks.push(await inspectSystemCredentialStore());
76
77
  const result = localEnvelope({ localReady: checks.every((check) => check.ok),
77
78
  authentication: "not_checked", installation: "not_checked", checks });
78
79
  result.target = config ? { environment: config.apiBaseUrl, appId: binding?.appId ?? null } : null;
@@ -0,0 +1,131 @@
1
+ $ErrorActionPreference = 'Stop'
2
+ $ProgressPreference = 'SilentlyContinue'
3
+ $PSModuleAutoLoadingPreference = 'None'
4
+
5
+ try {
6
+ if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
7
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_unavailable"}')
8
+ exit 0
9
+ }
10
+ Import-Module "$PSHOME\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1" -ErrorAction Stop
11
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
12
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
13
+ $request = [Console]::In.ReadToEnd() | ConvertFrom-Json
14
+ $keys = @($request.PSObject.Properties.Name)
15
+ if ($request.version -ne 1 -or $request.operation -notin @('get', 'set', 'delete', 'probe') -or
16
+ @($keys | Where-Object { $_ -notin @('version', 'operation', 'origin', 'credential') }).Count -ne 0 -or
17
+ $request.origin -isnot [string] -or $request.origin.Length -gt 2048) { throw 'invalid request' }
18
+ $uri = New-Object System.Uri($request.origin, [System.UriKind]::Absolute)
19
+ if ($uri.Scheme -notin @('http', 'https') -or $uri.UserInfo -or $uri.AbsolutePath -ne '/' -or $uri.Query -or $uri.Fragment) { throw 'invalid origin' }
20
+ if ($request.operation -eq 'set') {
21
+ if ($request.credential -isnot [string] -or $request.credential.Length -lt 1 -or $request.credential.Length -gt 512) { throw 'invalid credential' }
22
+ } elseif ($keys -contains 'credential') { throw 'unexpected credential' }
23
+
24
+ # 仅绑定固定 Win32 API;全部在内存中构造,避免运行时编译器对 Unicode TEMP 的依赖。
25
+ $assembly = [AppDomain]::CurrentDomain.DefineDynamicAssembly(
26
+ (New-Object Reflection.AssemblyName('ZhikeCredentialBridge')), [Reflection.Emit.AssemblyBuilderAccess]::Run)
27
+ $module = $assembly.DefineDynamicModule('ZhikeCredentialBridge')
28
+ $layout = [Reflection.TypeAttributes]::Public -bor [Reflection.TypeAttributes]::SequentialLayout -bor [Reflection.TypeAttributes]::Sealed
29
+ $structure = $module.DefineType('ZhikeCredential', $layout, [ValueType])
30
+ # 字段顺序对应 CREDENTIALW;指针由当前进程位数决定,FILETIME 作为 64 位值对齐。
31
+ $fields = @(
32
+ @('Flags', [uint32]), @('Type', [uint32]), @('TargetName', [IntPtr]), @('Comment', [IntPtr]),
33
+ @('LastWritten', [long]), @('CredentialBlobSize', [uint32]), @('CredentialBlob', [IntPtr]),
34
+ @('Persist', [uint32]), @('AttributeCount', [uint32]), @('Attributes', [IntPtr]),
35
+ @('TargetAlias', [IntPtr]), @('UserName', [IntPtr]))
36
+ foreach ($field in $fields) { [void]$structure.DefineField($field[0], $field[1], [Reflection.FieldAttributes]::Public) }
37
+ $credentialType = $structure.CreateType()
38
+ $binding = $module.DefineType('ZhikeNativeCredential', [Reflection.TypeAttributes]::Public)
39
+ $dllImport = [Runtime.InteropServices.DllImportAttribute]
40
+ $importFields = [Reflection.FieldInfo[]]@($dllImport.GetField('EntryPoint'), $dllImport.GetField('CharSet'), $dllImport.GetField('SetLastError'))
41
+ $signatures = @(
42
+ @('Read', 'CredReadW', [bool], [type[]]@([string], [uint32], [uint32], [IntPtr].MakeByRefType())),
43
+ @('Write', 'CredWriteW', [bool], [type[]]@([IntPtr], [uint32])),
44
+ @('Delete', 'CredDeleteW', [bool], [type[]]@([string], [uint32], [uint32])),
45
+ @('Free', 'CredFree', [void], [type[]]@([IntPtr])))
46
+ foreach ($signature in $signatures) {
47
+ $method = $binding.DefineMethod($signature[0], [Reflection.MethodAttributes]'Public, Static, PinvokeImpl', $signature[2], $signature[3])
48
+ $attribute = New-Object Reflection.Emit.CustomAttributeBuilder(
49
+ $dllImport.GetConstructor([type[]]@([string])), [object[]]@('advapi32.dll'),
50
+ $importFields, [object[]]@($signature[1], [Runtime.InteropServices.CharSet]::Unicode, $true))
51
+ $method.SetCustomAttribute($attribute)
52
+ $method.SetImplementationFlags([Reflection.MethodImplAttributes]::PreserveSig)
53
+ }
54
+ $native = $binding.CreateType()
55
+ $hash = [Security.Cryptography.SHA256]::Create()
56
+ try { $digest = [BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($request.origin))).Replace('-', '').ToLowerInvariant() }
57
+ finally { $hash.Dispose() }
58
+ $target = 'com.77hub.zhike.developer-cli/v1/' + $digest
59
+ $value = $null
60
+ switch ($request.operation) {
61
+ { $_ -in @('get', 'probe') } {
62
+ $pointer = [IntPtr]::Zero
63
+ if (-not $native::Read($target, 1, 0, [ref]$pointer)) {
64
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
65
+ if ($errorCode -ne 1168) { throw 'credential read failed' }
66
+ } else {
67
+ $blob = [IntPtr]::Zero
68
+ $blobSize = 0
69
+ try {
70
+ # 按固定结构的实际偏移读取,避免 PowerShell 在 PtrToStructure 的 Type/Object 重载间歧义。
71
+ $kind = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Type').ToInt32())
72
+ $persist = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'Persist').ToInt32())
73
+ $blobSize = [Runtime.InteropServices.Marshal]::ReadInt32($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlobSize').ToInt32())
74
+ $blob = [Runtime.InteropServices.Marshal]::ReadIntPtr($pointer, [Runtime.InteropServices.Marshal]::OffsetOf($credentialType, 'CredentialBlob').ToInt32())
75
+ # Generic / 本用户本机跨登录持久化;BlobSize 单位为字节,内容为 UTF-16LE。
76
+ if ($kind -ne 1 -or $persist -ne 2 -or $blobSize -lt 1 -or $blobSize -gt 1024 -or
77
+ $blobSize % 2 -ne 0 -or $blob -eq [IntPtr]::Zero) { throw 'invalid credential metadata' }
78
+ if ($request.operation -eq 'get') {
79
+ $value = [Runtime.InteropServices.Marshal]::PtrToStringUni($blob, [int]($blobSize / 2))
80
+ }
81
+ } finally {
82
+ if ($blob -ne [IntPtr]::Zero -and $blobSize -gt 0 -and $blobSize -le 1024) {
83
+ for ($i = 0; $i -lt $blobSize; $i++) { [Runtime.InteropServices.Marshal]::WriteByte($blob, $i, 0) }
84
+ }
85
+ $native::Free($pointer)
86
+ }
87
+ }
88
+ }
89
+ 'set' {
90
+ $bytes = [Text.Encoding]::Unicode.GetBytes($request.credential)
91
+ $blob = [IntPtr]::Zero
92
+ $record = [IntPtr]::Zero
93
+ $namePointer = [IntPtr]::Zero
94
+ $userPointer = [IntPtr]::Zero
95
+ try {
96
+ $blob = [Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
97
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
98
+ $namePointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($target)
99
+ $userPointer = [Runtime.InteropServices.Marshal]::StringToHGlobalUni($request.origin)
100
+ $credential = [Activator]::CreateInstance($credentialType)
101
+ $credential.Type = 1
102
+ $credential.TargetName = $namePointer
103
+ $credential.UserName = $userPointer
104
+ $credential.CredentialBlob = $blob
105
+ $credential.CredentialBlobSize = $bytes.Length
106
+ $credential.Persist = 2
107
+ $record = [Runtime.InteropServices.Marshal]::AllocHGlobal([Runtime.InteropServices.Marshal]::SizeOf($credential))
108
+ [Runtime.InteropServices.Marshal]::StructureToPtr($credential, $record, $false)
109
+ if (-not $native::Write($record, 0)) { throw 'credential write failed' }
110
+ } finally {
111
+ [Array]::Clear($bytes, 0, $bytes.Length)
112
+ if ($blob -ne [IntPtr]::Zero) {
113
+ [Runtime.InteropServices.Marshal]::Copy($bytes, 0, $blob, $bytes.Length)
114
+ [Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
115
+ }
116
+ foreach ($allocated in @($record, $namePointer, $userPointer)) {
117
+ if ($allocated -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeHGlobal($allocated) }
118
+ }
119
+ }
120
+ }
121
+ 'delete' {
122
+ if (-not $native::Delete($target, 1, 0)) {
123
+ $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
124
+ if ($errorCode -ne 1168) { throw 'credential delete failed' }
125
+ }
126
+ }
127
+ }
128
+ [Console]::Out.Write((@{ version = 1; ok = $true; value = $value } | ConvertTo-Json -Compress))
129
+ } catch {
130
+ [Console]::Out.Write('{"version":1,"ok":false,"code":"zhike_secure_store_failed"}')
131
+ }
@@ -0,0 +1,112 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { readFile } from "node:fs/promises";
4
+ import { win32 } from "node:path";
5
+
6
+ import { normalizeOrigin } from "../config.js";
7
+ import { ZhikeCliError } from "../errors.js";
8
+
9
+ let script;
10
+ const failure = () => new ZhikeCliError("zhike_secure_store_failed", "无法访问 Windows 安全凭据存储,请检查系统凭据库和 PowerShell 策略后重试");
11
+ const unavailable = () => new ZhikeCliError("zhike_secure_store_unavailable", "Windows 安全凭据适配不可用,需要系统 Windows PowerShell 5.1 及允许的原生 API 调用");
12
+
13
+ export class WindowsCredentialStore {
14
+ constructor({ runner = runWindowsCredentialBridge } = {}) {
15
+ this.runner = runner;
16
+ }
17
+
18
+ async get(environmentId) { return this.request("get", normalizeOrigin(environmentId, "Developer API")); }
19
+
20
+ async set(environmentId, refreshCredential) {
21
+ if (typeof refreshCredential !== "string" || !refreshCredential.length || refreshCredential.length > 512) throw failure();
22
+ await this.request("set", normalizeOrigin(environmentId, "Developer API"), refreshCredential);
23
+ }
24
+
25
+ async delete(environmentId) { await this.request("delete", normalizeOrigin(environmentId, "Developer API")); }
26
+
27
+ async probe() { await this.request("probe", `https://probe-${randomUUID()}.invalid`); }
28
+
29
+ async request(operation, origin, credential) {
30
+ try {
31
+ const result = await this.runner({ version: 1, operation, origin, ...(credential === undefined ? {} : { credential }) });
32
+ if (!result || result.version !== 1 || typeof result.ok !== "boolean" ||
33
+ Object.keys(result).some((key) => !["version", "ok", "value", "code"].includes(key))) throw failure();
34
+ if (!result.ok) {
35
+ if (result.code === "zhike_secure_store_unavailable") throw unavailable();
36
+ throw failure();
37
+ }
38
+ if (operation === "get") {
39
+ if (result.value !== null && (typeof result.value !== "string" || !result.value.length || result.value.length > 512)) throw failure();
40
+ return result.value;
41
+ }
42
+ if (result.value !== null) throw failure();
43
+ } catch (error) {
44
+ if (error instanceof ZhikeCliError) throw error;
45
+ throw failure();
46
+ }
47
+ }
48
+ }
49
+
50
+ /** 固定程序/脚本与秘密数据分离;子进程输出不得原文进入 CLI 错误。 */
51
+ export async function runWindowsCredentialBridge(request, {
52
+ spawnImpl = spawn, systemRoot = process.env.SystemRoot, environment = process.env,
53
+ timeoutMs = 10_000, maxOutputBytes = 65_536,
54
+ } = {}) {
55
+ if (!systemRoot || !win32.isAbsolute(systemRoot)) throw unavailable();
56
+ let source;
57
+ try { source = await (script ??= readFile(new URL("./windows-credential-bridge.ps1", import.meta.url), "utf8")); } catch { throw unavailable(); }
58
+ const executable = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
59
+ const encoded = Buffer.from(source, "utf16le").toString("base64");
60
+ const env = Object.fromEntries(Object.entries(environment).filter(([key]) =>
61
+ ["systemroot", "windir", "temp", "tmp", "path", "comspec", "systemdrive",
62
+ "userprofile", "appdata", "localappdata", "homedrive", "homepath", "username", "userdomain",
63
+ "programfiles", "programfiles(x86)", "programw6432", "programdata", "allusersprofile",
64
+ "pathext", "computername", "processor_architecture", "processor_identifier", "processor_level",
65
+ "processor_revision", "number_of_processors", "os"].includes(key.toLowerCase())));
66
+ // ConvertFrom-Json / Add-Type 的模块解析只使用系统目录,不加载项目或用户自定义模块。
67
+ // 固定子进程不需要持久化命令发现缓存;避免依赖 runner/用户的缓存路径或锁。
68
+ env.PSModuleAnalysisCachePath = "NUL";
69
+ env.PSModulePath = win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "Modules");
70
+ return new Promise((resolve, reject) => {
71
+ let child;
72
+ let output = "";
73
+ let outputSize = 0;
74
+ let settled = false;
75
+ let timer;
76
+ const finish = (error, result) => {
77
+ if (settled) return;
78
+ settled = true;
79
+ clearTimeout(timer);
80
+ output = "";
81
+ if (error) reject(error); else resolve(result);
82
+ };
83
+ const terminate = () => {
84
+ if (Number.isInteger(child?.pid) && process.platform === "win32") {
85
+ execFile(win32.join(systemRoot, "System32", "taskkill.exe"), ["/PID", String(child.pid), "/T", "/F"],
86
+ { windowsHide: true, timeout: 3_000 }, () => child.kill());
87
+ } else child?.kill();
88
+ };
89
+ try {
90
+ child = spawnImpl(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encoded],
91
+ { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env });
92
+ } catch { finish(unavailable()); return; }
93
+ timer = setTimeout(() => { terminate(); finish(failure()); }, timeoutMs);
94
+ const collect = (bytes, keep) => {
95
+ if (settled) return;
96
+ outputSize += Buffer.byteLength(bytes);
97
+ if (outputSize > maxOutputBytes) { terminate(); finish(failure()); return; }
98
+ if (keep) output += bytes;
99
+ };
100
+ child.stdout.setEncoding("utf8");
101
+ child.stdout.on("data", (bytes) => collect(bytes, true));
102
+ child.stderr.on("data", (bytes) => collect(bytes, false));
103
+ child.on("error", () => finish(unavailable()));
104
+ child.stdin.on("error", () => { terminate(); finish(failure()); });
105
+ child.on("close", (code) => {
106
+ if (settled) return;
107
+ if (code !== 0) { finish(failure()); return; }
108
+ try { finish(null, JSON.parse(output)); } catch { finish(failure()); }
109
+ });
110
+ child.stdin.end(JSON.stringify(request), "utf8");
111
+ });
112
+ }
@@ -0,0 +1,22 @@
1
+ /** 静态信息进入不可变 Manifest;工具集合由同目录 .tools.ts 唯一决定。 */
2
+ export interface AppAgentDefinition {
3
+ key: string;
4
+ name: string;
5
+ /** App 提供的系统提示词,最多 16KiB UTF-8,不交付 Client Worker。 */
6
+ systemPrompt: string;
7
+ }
8
+ export type ToolSchema = Readonly<Record<string, unknown>>;
9
+ export interface AppToolDefinition<Input, Output> {
10
+ key: string;
11
+ name: string;
12
+ description: string;
13
+ /** 有界 JSON Schema;平台在模型调用和结果交付时再次严格验证。 */
14
+ inputSchema: ToolSchema;
15
+ outputSchema: ToolSchema;
16
+ /** 默认 none;output 只允许成功且通过校验的结果进入实时 callback。 */
17
+ clientResult?: 'none' | 'output';
18
+ /** 仅在所属安装的服务端执行环境运行,凭据沿用现有 Server SDK。 */
19
+ execute(input: Input): Output | Promise<Output>;
20
+ }
21
+ export function defineAgent(definition: AppAgentDefinition): Readonly<AppAgentDefinition>;
22
+ export function defineTool<Input = Record<string, unknown>, Output = unknown>(definition: AppToolDefinition<Input, Output>): Readonly<AppToolDefinition<Input, Output>>;
@@ -0,0 +1,30 @@
1
+ const KEY = /^[a-z][a-z0-9-]{0,63}$/;
2
+
3
+ function fields(value, required, optional = []) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
5
+ required.some(key => !Object.hasOwn(value, key)) ||
6
+ Object.keys(value).some(key => ![...required, ...optional].includes(key))) {
7
+ throw new TypeError("agent_definition_invalid");
8
+ }
9
+ if (typeof value.key !== "string" || value.key.trim() !== value.key || !KEY.test(value.key) || typeof value.name !== "string" ||
10
+ !value.name.trim() || value.name.length > 80) throw new TypeError("agent_definition_invalid");
11
+ }
12
+
13
+ export function defineAgent(definition) {
14
+ fields(definition, ["key", "name", "systemPrompt"]);
15
+ if (typeof definition.systemPrompt !== "string" || !definition.systemPrompt.trim() ||
16
+ new TextEncoder().encode(definition.systemPrompt).length > 16384) {
17
+ throw new TypeError("agent_definition_invalid");
18
+ }
19
+ return Object.freeze({ ...definition });
20
+ }
21
+
22
+ export function defineTool(definition) {
23
+ fields(definition, ["key", "name", "description", "inputSchema", "outputSchema", "execute"], ["clientResult"]);
24
+ if (typeof definition.execute !== "function" || typeof definition.description !== "string" ||
25
+ !definition.description.trim() || definition.description.length > 1200 ||
26
+ ![undefined, "none", "output"].includes(definition.clientResult)) {
27
+ throw new TypeError("agent_tool_definition_invalid");
28
+ }
29
+ return Object.freeze({ ...definition, clientResult: definition.clientResult ?? "none" });
30
+ }
@@ -1,3 +1,4 @@
1
+ export { defineAgent, defineTool, type AppAgentDefinition, type AppToolDefinition, type ToolSchema } from "./agents/index.js";
1
2
  import type { JsonValue } from "../index.js";
2
3
  export { defineWorkflowStep, defineWorkflowStepExecute } from "./steps/index.js";
3
4
  export type { WorkflowObjectSchema, WorkflowScalarSchema, WorkflowStepDefinition, WorkflowStepOutcome, WorkflowStepResult, WorkflowSchemaValue } from "./steps/index.js";
@@ -1,3 +1,4 @@
1
+ export { defineAgent, defineTool } from "./agents/index.js";
1
2
  const SERVER_FUNCTION_MARKER = Symbol.for("zhike.server-function.v1");
2
3
  export { defineWorkflowStep, defineWorkflowStepExecute } from "./steps/index.js";
3
4
  export { defineWorkflowTrigger, defineWorkflowTriggerActivate, defineWorkflowTriggerOnEvent, defineWorkflowTriggerDeactivate } from "./triggers/index.js";