@77-zhike/app-sdk 0.2.0-d7b1.2 → 0.2.0-d7b1.20

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.
Files changed (96) hide show
  1. package/README.md +281 -0
  2. package/package.json +17 -3
  3. package/src/cli/api-client.js +4 -0
  4. package/src/cli/browser-auth.js +64 -18
  5. package/src/cli/credential-store.js +21 -0
  6. package/src/cli/development-logs.js +5 -2
  7. package/src/cli/index.js +59 -42
  8. package/src/cli/local-commands.js +6 -3
  9. package/src/cli/logs.js +56 -0
  10. package/src/cli/project-init.js +111 -179
  11. package/src/cli/windows-credential-bridge.ps1 +131 -0
  12. package/src/cli/windows-credential-store.js +112 -0
  13. package/src/client/analysis.d.ts +4 -0
  14. package/src/client/analysis.js +12 -0
  15. package/src/client/destinations.d.ts +25 -0
  16. package/src/client/destinations.js +70 -0
  17. package/src/client/documents.d.ts +21 -0
  18. package/src/client/documents.js +39 -0
  19. package/src/client/external-url.js +9 -0
  20. package/src/client/index.d.ts +225 -3
  21. package/src/client/index.js +75 -3
  22. package/src/client/interactions.d.ts +23 -3
  23. package/src/client/interactions.js +53 -3
  24. package/src/client/points.generated.js +56 -0
  25. package/src/config.js +11 -7
  26. package/src/forms/array-contract.d.ts +32 -0
  27. package/src/forms/array-contract.js +138 -0
  28. package/src/forms/components.generated.js +2 -2
  29. package/src/forms/factory.js +21 -7
  30. package/src/forms/field-array.d.ts +16 -0
  31. package/src/forms/field-array.js +52 -0
  32. package/src/forms/index.d.ts +74 -3
  33. package/src/forms/index.js +2 -0
  34. package/src/forms/options.d.ts +7 -2
  35. package/src/forms/options.js +8 -1
  36. package/src/forms/record-options.d.ts +28 -0
  37. package/src/forms/record-options.js +92 -0
  38. package/src/forms/schema.d.ts +73 -9
  39. package/src/forms/schema.js +128 -0
  40. package/src/forms/state.d.ts +30 -0
  41. package/src/forms/state.js +82 -0
  42. package/src/forms/validation.d.ts +9 -0
  43. package/src/forms/validation.js +81 -0
  44. package/src/forms/values.d.ts +5 -0
  45. package/src/forms/values.js +38 -0
  46. package/src/jsx-dev-runtime.d.ts +2 -0
  47. package/src/jsx-dev-runtime.js +1 -0
  48. package/src/jsx-runtime.d.ts +2 -0
  49. package/src/jsx-runtime.js +1 -0
  50. package/src/server/agents/index.d.ts +62 -0
  51. package/src/server/agents/index.js +40 -0
  52. package/src/server/index.d.ts +60 -5
  53. package/src/server/index.js +43 -0
  54. package/src/server/steps/index.d.ts +71 -0
  55. package/src/server/steps/index.js +15 -0
  56. package/src/server/triggers/index.d.ts +68 -0
  57. package/src/server/triggers/index.js +27 -0
  58. package/src/server/workflow-blocks/index.d.ts +72 -0
  59. package/src/server/workflow-blocks/index.js +32 -0
  60. package/src/toolchain/agents/discovery.js +126 -0
  61. package/src/toolchain/agents/exposure.js +31 -0
  62. package/src/toolchain/agents/manifest.js +84 -0
  63. package/src/toolchain/ai-capabilities.js +69 -0
  64. package/src/toolchain/allowed-packages.json +7 -0
  65. package/src/toolchain/dependency-policy.js +181 -0
  66. package/src/toolchain/index.js +0 -88
  67. package/src/toolchain/real-build.js +135 -27
  68. package/src/toolchain/source-discovery.js +330 -81
  69. package/src/toolchain/workflow-blocks/discovery.js +133 -0
  70. package/src/toolchain/workflow-steps/manifest.js +99 -0
  71. package/src/toolchain/workflow-triggers/manifest.js +47 -0
  72. package/src/ui/components.generated.js +4 -1
  73. package/src/ui/conversation.d.ts +81 -0
  74. package/src/ui/conversation.js +82 -0
  75. package/src/ui/data-table-model.d.ts +14 -0
  76. package/src/ui/data-table-model.js +98 -0
  77. package/src/ui/data-table.d.ts +70 -0
  78. package/src/ui/data-table.js +74 -0
  79. package/src/ui/events.d.ts +10 -0
  80. package/src/ui/factory.js +87 -3
  81. package/src/ui/index.d.ts +351 -8
  82. package/src/ui/index.js +1 -1
  83. package/src/ui/jsx.generated.d.ts +359 -0
  84. package/src/ui/native-dom.d.ts +49 -0
  85. package/src/ui/native-dom.js +44 -0
  86. package/src/ui/scroll-viewport.d.ts +22 -0
  87. package/src/ui/scroll-viewport.js +23 -0
  88. package/src/ui/slider.d.ts +27 -0
  89. package/src/ui/slider.js +26 -0
  90. package/src/ui/style.d.ts +5 -0
  91. package/src/ui/style.generated.js +2 -0
  92. package/src/ui/style.js +15 -0
  93. package/src/ui/text-selection.d.ts +22 -0
  94. package/src/ui/text-selection.js +23 -0
  95. package/src/ui/validation.js +86 -0
  96. package/src/cli/validate-bundle.js +0 -36
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # 智客 App SDK 与 CLI
2
+
3
+ ## 正文批注与待发同步
4
+
5
+ `TextSelection` 可接收有界 `marks`(id、UTF-16区间、逐字text、1至20的ordinal)与 `onMarkActivate`;Host默认保留编号,通过当前view锚点打开Popover时才高亮对应原文,关闭后清除高亮,点击编号可重新编辑;不向App交付DOM或坐标。通过 `useConversation().state.references ?? []` 派生标记,与Composer待发引用保持同源。
6
+
7
+ annotation的comment允许空字符串,支持“先加入对话、再编辑评论”。保存更新同id,取消保留已加入引用,删除通过removeReference同步移除。仅引用原文而无评论时需在主输入说明要求;至少一条有评论才支持缺省正文发送。`form.TextArea`新增可选labelHidden,隐藏视觉标题但保留可访问名称。使用这些增量前须同步升级SDK、Runtime与双端Host,不能单独向旧宿主发布App。
8
+
9
+ ## 操作系统与 Windows 候选
10
+
11
+ 当前 npm 发布版的 Developer CLI 仅提供 macOS Keychain 适配。本开发分支新增 Windows Credential Manager 候选:复用现有浏览器 PKCE 和 Session,使用系统 Windows PowerShell 5.1 固定桥接,refresh credential 按当前用户和 API origin 存储。
12
+
13
+ Windows 候选通过独立的 `SDK Windows 自动化验证(不发布)` Actions 检查,分别运行 Node 22/24。测试制品仅供验证,未发布 npm;Windows 11 桌面、系统默认浏览器弹起、双用户和注销重登暂未覆盖。完整结果与边界见 `openspec/changes/windows-app-developer-cli/`。
14
+
15
+ `zhike doctor --json` 的 `secureStore` 检查表示当前机制可用性,`writeAccess: not_checked` 和 `authentication: not_checked` 不证明凭据已保存、登录有效或业务可用。系统策略禁止桥接时会返回安全存储错误,不修改系统策略或降级为明文文件。
16
+
17
+ ## 获准依赖
18
+
19
+ 工具链从 `src/toolchain/allowed-packages.json` 读取平台维护的包白名单。目前允许 `@77-zhike/qiqi-openapi@0.1.0` 的服务端包根入口 `.`;前端不可导入。项目安装依赖后直接使用静态 ESM import,真实解析的包名、精确版本、入口及间接模块在打包时核对,代码合并进 Artifact。App 的 Manifest、Settings 或 `package.json` 不能扩展名单。
20
+
21
+ Node builtin、CommonJS、动态 import、原生模块、未获准包和路径越界仍禁止。项目内 `file:` 包可用,但同名同版本不是来源证明。白名单只控制构建兼容性;不替代运行时沙箱、网络策略、租户隔离及 API 权限,不进行运行时 npm 安装。
22
+
23
+ ## 统一构建
24
+
25
+ `zhike dev` 是监听、构建、上传和应用开发安装的快捷工作流,不是另一种运行时。
26
+ 它与 `zhike build` 使用相同的 JSX 编译、压缩和 production 条件;仅额外携带源码映射。
27
+ 修复前已上传的开发产物不会自动改变,升级 SDK 后须重新运行 dev 或重新构建上传。
28
+
29
+ ## 查询历史日志
30
+
31
+ 在包含 `.zhike/project.json` 的 App 项目目录中执行:
32
+
33
+ ```sh
34
+ zhike logs --workspace <workspace-id> --since 1h
35
+ zhike logs --workspace <workspace-id> --since 1d --level error --keyword timeout --json
36
+ ```
37
+
38
+ - 使用项目绑定的 App 和环境、现有 Developer CLI 登录身份;不会构建、上传或安装 App。
39
+ - 默认查询最近一小时,最多最近三天,与 Console 历史查询一致。
40
+ - `--since` 接受带时区的 ISO 时间或 `30m`、`1h`、`1d` 等相对时间;`--until` 接受带时区的 ISO 时间。
41
+ - `--level` 为 `log`、`warn` 或 `error`;`--version` 指定执行版本,`--keyword` 搜索日志正文。
42
+ - 每次读取一页,按从新到旧排列;`--limit` 为 1..100,默认 100。
43
+ - 下一页使用返回的 `next_cursor` 作为 `--cursor`,同时保持 Workspace、过滤条件及返回的 `from/to` 窗口不变。
44
+ - `--json` 输出单个 JSON 结果;失败进程返回非零状态。日志正文是不可信内容,不应当作命令执行。
45
+ - 服务端需要部署支持 `/api/developer/cli/apps/{app_id}/logs` 的 Developer Server;旧服务端不会自动回退到 Web 凭据或实时订阅。
46
+
47
+ `zhike dev` 的实时订阅仍是独立用途,七天断线补读窗口不代表历史查询可超过三天。
48
+
49
+ ## Object Action 与 Bulk Record Action
50
+
51
+ 对象列表上的两类 Action 仍使用 `src/app/extensions/<key>/extension.tsx`:
52
+
53
+ ```tsx
54
+ import { defineFrontendExtension, showToast } from '@77-zhike/app-sdk/client'
55
+
56
+ export default defineFrontendExtension({
57
+ key: 'sync-selected-customers',
58
+ type: 'bulk-record-action',
59
+ label: '同步选中客户',
60
+ objects: ['accounts'],
61
+ async onTrigger({ object, recordIds }) {
62
+ await syncCustomers({ object, recordIds: [...recordIds] })
63
+ await showToast({ message: `已提交 ${recordIds.length} 条客户`, tone: 'success' })
64
+ },
65
+ })
66
+ ```
67
+
68
+ - `object-action` 显示在列表右上角对象操作菜单,只接收 `{ object }`。
69
+ - `bulk-record-action` 只在产品列表允许当前页显式多选时出现,接收 `{ object, recordIds }`;`recordIds` 为点击瞬间按当前页顺序冻结的 1~100 个公开 ID。
70
+ - `objects` 只允许 `accounts`、`contacts`、`opportunities` 和公开的 `custom_*` slug。任务、跟进记录、笔记不是 SDK Record Surface,不能用于 `record-action`、`object-action` 或 `bulk-record-action`。SDK 不暴露查询、权限、revision、内部 definition ID、系统 mutation 或产品 Action slot。
71
+ - Web/H5 使用同一产品目录和执行合同,但分别渲染菜单、Dialog/Sheet 与选择交互。没有可执行 Bulk Action 时不会出现空选择入口。
72
+ - Action 发现不会启动 Worker;点击后才校验当前 Installation 并 Lazy 执行。Action 本期不能调用 `navigate()`。
73
+ - Destination 的可导航对象与 Record 扩展白名单是两个独立合同;某类产品对象可以被打开,不代表 App 可以向它注册 Record Action。
74
+
75
+ ## Record Widget、Record Section 与 Record Tab
76
+
77
+ 记录页面组件继续使用 `src/app/extensions/<key>/extension.tsx`。Widget 由管理员加入卡片区;Record Section 进入统一 Section 目录,当前列表式设置默认将新 placement 加入 Overview;Tab 安装后进入产品 Tab 目录。三者都只接收平台签发的公开记录身份:
78
+
79
+ ```tsx
80
+ import {
81
+ defineFrontendExtension,
82
+ type RecordComponentProps,
83
+ } from '@77-zhike/app-sdk/client'
84
+ import { Badge, Widget } from '@77-zhike/app-sdk/ui'
85
+
86
+ function HealthWidget({ object, recordId }: RecordComponentProps) {
87
+ return (
88
+ <Widget.TextWidget onTrigger={() => openHealthDetails({ object, recordId })}>
89
+ <Widget.Title>客户健康度</Widget.Title>
90
+ <Widget.Text.Primary>92</Widget.Text.Primary>
91
+ <Widget.Text.Secondary>{`记录:${recordId}`}</Widget.Text.Secondary>
92
+ <Widget.Decoration>
93
+ <Badge tone="success">正常</Badge>
94
+ </Widget.Decoration>
95
+ </Widget.TextWidget>
96
+ )
97
+ }
98
+
99
+ export default defineFrontendExtension({
100
+ key: 'customer-health',
101
+ type: 'record-widget',
102
+ label: '客户健康度',
103
+ objects: ['accounts'],
104
+ Widget: HealthWidget,
105
+ })
106
+ ```
107
+
108
+ Record Section 使用同一声明形态,把 `type` 改为 `record-section` 并以 `Section` 提供组件;它按 final plan 在 placement 指定的 Section 区域渲染。`recordDetails.supportingSections` 与 `overview.sections` 只是位置,共用同一 capability 和 View。Record Tab 则使用 `record-tab` 和 `Tab`。`objects` 省略时适用于客户、联系人、商机和全部自定义对象;具体 `custom_*` 只匹配同名公开对象。任务、跟进记录和笔记不是 SDK Record Surface。
109
+
110
+ ```tsx
111
+ import {
112
+ defineFrontendExtension,
113
+ type RecordComponentProps,
114
+ } from '@77-zhike/app-sdk/client'
115
+ import { Button, Stack, TextBlock } from '@77-zhike/app-sdk/ui'
116
+
117
+ function BusinessSummary({ object, recordId }: RecordComponentProps) {
118
+ return (
119
+ <Stack gap="medium">
120
+ <TextBlock>当前记录:{`${object} / ${recordId}`}</TextBlock>
121
+ <Button onClick={() => refreshSummary({ object, recordId })}>刷新摘要</Button>
122
+ </Stack>
123
+ )
124
+ }
125
+
126
+ export default defineFrontendExtension({
127
+ key: 'business-summary',
128
+ type: 'record-section',
129
+ label: '经营摘要',
130
+ objects: ['accounts', 'opportunities'],
131
+ Section: BusinessSummary,
132
+ })
133
+ ```
134
+
135
+ `record-widget` 表示 Overview 顶部卡片。唯一根节点必须是 `Widget.TextWidget`,首次数据尚未就绪时可以返回 `Widget.Loading`。`Widget.Title`、`Widget.Text.Primary`、`Widget.Text.Secondary` 与可选的 `Widget.Decoration` 只描述卡片语义;其中 Decoration 只接受 `Badge` 或 `StatusBadge`。Web/H5 Host 使用各自 `components/ui` 实现外框、间距、字体、可信归属与 loading/error,App 不要在根节点再次套 `Card` 或自行模拟卡片样式。产品原生重点字段也复用同一套 Overview Widget 原子组件,因此 App 与产品卡片在同一端保持一致。
136
+
137
+ `Widget.TextWidget.onTrigger` 用于整卡点击,Host 会提供 pending 单飞与失败反馈;发现和渲染本身不会触发该回调。未提供 `Widget.Title` 时,Host 使用 capability label 作为受信回退标题。全宽内容必须使用 `record-section`,不能复用 `record-widget`。
138
+
139
+ - 产品只保存稳定 capability placement,不保存 App 版本或 Worker 身份;升级、卸载后重装仍可恢复原布局。Widget、Section、Tab 分别进入固定共享贡献点,不为每个组件创建新贡献点。
140
+ - Web/H5 使用同一份布局和 `{ object, recordId }`,但分别渲染适合当前宿主的 UI。H5 不支持的 Forms/`LookupCell` 只让当前 Widget/Section/Tab 进入 `unsupported`,不会拖垮详情或同 App 的其他组件。
141
+ - Widget/Section/Tab 标签和设置候选的发现不启动 Worker;可见 Widget/Section 或当前选中 Tab 才挂载 App View。Section 挂载后可自行初始化查询,Host 不提供数据预取合同。Server Function 仍必须由用户在已挂载组件中显式触发。
142
+ - App 不能声明 capability ID、布局位置、排序 tier、产品 route、`viewName`、Workspace/Installation 身份或 Native availability metadata。
143
+ - 当前 Record Page 三类组件不开放命令式 `navigate()`;详情 Tab 的 URL 与刷新恢复由产品 Host 统一管理。
144
+
145
+ ## Workspace Page
146
+
147
+ Page 源码放在 `src/app/pages/<slug>/page.tsx`。一级目录名就是稳定 slug;Page 只声明名称、排序、布局和纯 React 组件,不接收 Router、Workspace、鉴权 token 或宿主 props。
148
+
149
+ ```tsx
150
+ import {
151
+ definePage,
152
+ Destinations,
153
+ } from '@77-zhike/app-sdk/client'
154
+ import {
155
+ Card,
156
+ ZhikeLink,
157
+ Stack,
158
+ TextBlock,
159
+ } from '@77-zhike/app-sdk/ui'
160
+
161
+ function QueuePage() {
162
+ return (
163
+ <Stack gap="large">
164
+ <Card title="任务队列">
165
+ <TextBlock>当前没有待处理任务。</TextBlock>
166
+ </Card>
167
+ <ZhikeLink destination={Destinations.appPage('overview')}>
168
+ 返回总览
169
+ </ZhikeLink>
170
+ </Stack>
171
+ )
172
+ }
173
+
174
+ export default definePage({
175
+ name: '任务队列',
176
+ order: 20,
177
+ variant: 'centered',
178
+ Page: QueuePage,
179
+ })
180
+ ```
181
+
182
+ 同一 App 内,有 `order` 的 Page 排在前面并按 `order + slug` 稳定排序;未声明 `order` 的 Page 排在后面并按 slug 排序。`name` trim 后长度为 1~80 个 Unicode code points。`variant: 'full' | 'centered'` 只表达布局语义,Desktop 与 H5 分别决定具体页面壳。
183
+
184
+ Page UI 必须使用 `@77-zhike/app-sdk/ui` 和 `@77-zhike/app-sdk/forms` 的公开组件,不能依赖 DOM、React Router 或自行复制产品控件。当前 Desktop 支持 Components 与 Forms;H5 支持 TextBlock、布局、反馈、Button、Tabs、Table、Card、Grid、EmptyState、Link、ZhikeLink 等 portable Components,并支持 Forms,暂不支持 `LookupCell`。含有不支持能力的 Page 在 H5 会整体显示“不支持”,不会静默丢节点或部分提交。
185
+
186
+ 导航只提交结构化 Destination:
187
+
188
+ - `Destinations.appPage(slug)` 打开当前 App 的另一个 Page。
189
+ - `Destinations.record({ object, recordId })` 支持 `accounts`、`contacts`、`opportunities`、`tasks`、`activities` 和当前 Workspace 可见的 `custom_*` 公开 API slug。
190
+ - `ZhikeLink` 保留普通链接、复制地址和新标签行为;普通点击仍会在跳转前重验当前 Page 身份。外部 HTTPS 使用 `Link`。
191
+ - `navigate()` 当前只允许从已挂载的 Page 调用,三类 Action 都不具备该能力。
192
+
193
+ 目录展示不会启动 Worker。首次打开 Page 时,宿主才会下载并校验 Manifest/Bundle、启动 Installation Worker;因此“Apps 菜单可见”不等于代码已经验证可运行。页面数据读写仍应通过受控 Server Function 完成。
194
+
195
+ ## 当前 Workspace 用户
196
+
197
+ Client 与用户触发的 Server Function 都可以同步读取当前 Workspace 成员:
198
+
199
+ ```tsx
200
+ import { getCurrentUser } from '@77-zhike/app-sdk/client'
201
+
202
+ const currentUser = getCurrentUser()
203
+ // currentUser.workspaceMemberId 是 Workspace 成员 ID,不是平台全局 User ID。
204
+ ```
205
+
206
+ ```ts
207
+ import { getCurrentUser } from '@77-zhike/app-sdk/server'
208
+
209
+ export default async function readForCurrentMember() {
210
+ const currentUser = getCurrentUser()
211
+ return { workspaceMemberId: currentUser.workspaceMemberId }
212
+ }
213
+ ```
214
+
215
+ 返回值只包含 `workspaceMemberId`、`displayName`、`primaryEmailAddress`、`avatarUrl`,且为只读快照。它不包含 Token、角色、权限集合或平台全局 User ID;App 仍须通过 Server Function 与公开 API 完成数据授权,不能用该快照自行推断权限。Lifecycle、Webhook 等无人触发入口没有当前用户,调用时抛出 `current_user_unavailable`;不得回退到管理员、安装者或 Connection 所有者。
216
+
217
+ ## 基础UI组件扩展
218
+
219
+ 新增Avatar、DescriptionList、Divider、Json、外部Link、StatusBadge、TextBlock与Typography,补齐Banner动作、Badge颜色、Card呈现、Grid容器模式、EmptyState操作编排和Table.HeaderCell。复合写法使用同一节点。
220
+
221
+ Desktop与H5注册的组件合同中,`ZhikeLink` 仍仅限Page,`Link` 是受治理的外部HTTPS链接;H5支持Forms,仍不开放Settings入口。新包要求`remote-react.v3`,发布前必须先部署对应Host及Bundle准入合同。
222
+
223
+ 新增组件的日常验证:修改Core `components/contracts.mjs`后执行`npm run generate:components --workspace @77-zhike/app-sdk`和`npm run check:components --workspace @77-zhike/app-sdk`,同步双端manifest与adapter。App项目运行`zhike dev --workspace <workspace-id>`,从对应Workspace的Apps入口打开Page;Desktop Settings仅验证其允许的子集。
224
+
225
+ 仓库内还提供无业务数据库的独立组件预览,命令和证据边界见`integration-app-runtime/verification/fixtures/ui-parity/README.md`。该预览不替代真实安装、权限与Server Function验证。
226
+
227
+ ## 表单组件
228
+
229
+ Desktop 与 H5 均支持 17 个可视 Forms 节点,以及只读 `WithState`。文本、多行、数字、日期、时间、选择、分组等输入复用各端基础 UI;Host 继续持有 RHF 状态,组件选择不会自动保存。数字空值为 null,日期为 YYYY-MM-DD,日期时间提交为带时区 ISO;旧 string/boolean/enum 语义保留。
230
+
231
+ `RecordCombobox` 默认只需在 schema 声明单个 `objectSlug`(或 `Forms.record(objectSlug)`),由 Host 使用当前安装的 Records Read 权限读取公开记录,保存值是 `{ objectSlug, recordId }`。空词沿用 Query 分页,非空词使用最多 25 条、无分页的 Record Search,已选值独立 Get。自定义数据源才提供 `RecordOptionsProvider.search/getOption` 并可自行分页;业务有效性及显式保存仍由消费方负责。`WithState` 仅订阅指定的 values/errors/submitting,不提供状态修改或程序化提交。
232
+
233
+ 可复制的非 CRM 示例与真实 tarball/Worker/SF/Settings/PostgreSQL 验证入口见 `integration-app-runtime/verification/fixtures/forms-parity/README.md`。当前尚未发布,所有新增 Forms 能力并入 remote-react.v3,ABI=2 不变;沿用 v1/v2 合同校验。SDK 与双端 Host 应整体交付,不引入新的版本分叉。
234
+
235
+ ## Dialog
236
+
237
+ 通过 `showDialog({ title, Dialog, size })` 打开临时界面;内容组件接收 `hideDialog()`。Desktop 复用 Dialog,H5 复用 Sheet,继续组合已有 UI/Form。可选 `size` 为 `default | wide | content`:默认保持原规格,宽版最大 1120px,自适应按内容固有宽度展开并受视口约束;H5 均保持全屏 Sheet。当前不提供 DialogList、嵌套 Dialog 或固定 Footer,按钮直接放在正文中。
238
+
239
+ 从 Page/Settings 事件或 Record Action 调用。普通关闭完成 Promise;身份失效或打开失败拒绝。Action 正常返回后已受理 Dialog 可继续使用;保存必须由 App 显式调用 Server Function。Dialog 内不提供 Page 导航身份。详见开发者文档 `app-sdk/dialogs/show-dialog`。
240
+
241
+ ## 当前成员的只读分析与 Chart
242
+
243
+ App Page 可从 `@77-zhike/app-sdk/client` 调用 `queryAnalysis({ operation, input })`。`operation` 仅允许 `list`、`get`、`execute`;输入沿用公开 MCP 分析目录、详情和执行合同。先分页读取获权目录,再从定义详情的 `capability` 与 `queryHints` 构造执行请求。禁止传入成员、租户、URL、任意 SQL 或内联定义。
244
+
245
+ 此能力由 Host 注入当前成员和已安装 Page 的可信身份,不经 App 机器身份执行。Settings、Action 与 Dialog 不支持。读取前后均复验安装资格;页面关闭或切换工作空间时取消请求,迟到结果不交付。不可用历史、缺数据和权限错误须作为明确状态呈现。
246
+
247
+ ```tsx
248
+ import { queryAnalysis } from '@77-zhike/app-sdk/client';
249
+ import { Chart } from '@77-zhike/app-sdk/ui';
250
+
251
+ const directory = await queryAnalysis({ operation: 'list', input: { limit: 20, offset: 0 } });
252
+ // 执行输入依据详情发布的维度和 Widget 能力构造。
253
+ const trend = <Chart label="经营趋势" variant="area" format="currency" height={280}
254
+ data={{ labels: ['一月', '二月'], series: [{ id: 'amount', label: '金额', values: [120000, null] }] }} />;
255
+ ```
256
+
257
+ Chart 仅接受纯数据 `labels` 与 `series`,不接受脚本、HTML 或任意样式。最多 200 个标签、4 条系列,32 KiB;系列 ID 唯一,各系列长度与标签相等。`null` 保留缺失断点;标签可重复,按位置区分。`variant` 支持 `line | bar | area`,`format` 支持 `number | currency | percent`,`height` 为 200、280 或 360。比例以 0..1 传入。`onSelect({ seriesId, index })` 支持提示和数据表选择;Web/H5 共用组件,图例与可展开数据表由 Host 提供。
258
+
259
+ ### Dashboard 与高级分析呈现
260
+
261
+ `DashboardSurface` 提供固定 `graphite` 业务色板,深浅主题跟随主应用;`DashboardLayout` 提供 `mainAside`、`equal`、`three` 响应式布局,配合 `DashboardPanel` 和 `MetricReadout`。这些组件不接受任意 CSS、DOM 或主题变量。
262
+
263
+ `AnalysisView` 接收同一资产的 `queryAnalysis(get)` 定义和 `queryAnalysis(execute)` 结果。Host 复用已有分析渲染器,支持同期群、漏斗、基础/增强/堆叠/表格阶梯与历史构成瀑布;状态分析可切换度量,并查看全部度量原始结果。高级呈现须符合定义开放的能力,历史覆盖不足须由调用方明确显示。
264
+
265
+ 定义与结果分别限制为 256 KiB / 20,000 项 / 24 层;状态分析最多 400 行、24 个唯一度量,结果单元格最多 2,400 个。拒绝跨资产结果、脚本、非有限值和无界数据。`onSelect` 仅返回受控数据选择,不开放内部记录路由或网络请求。使用这些组件需要同步升级本仓库的 SDK 和双端 Host。
266
+
267
+ ### 精确数字的表格录入
268
+
269
+ `form.TextInput` 可选 `labelHidden` 和 `inputMode="numeric" | "decimal"`。隐藏标题仍保留与控件关联的读屏标签,默认标题继续显示,字段错误和说明仍显示。`inputMode` 只提示键盘,不转换、过滤或格式化值,不替代业务校验;需要保留十进制字符串或未完成输入时使用,数值型字段继续使用 `NumberInput`。
270
+
271
+ ## App 对话输入部件
272
+
273
+ `Conversation.Root` 内自由组合 Messages、History、NewButton、Status、Composer。History 默认使用与 automation 相同的图标弹出列表;Composer 默认带附件/图片上传和语音回填,转写后仍需用户发送。输入能力由 Host 提供,不把 File/音频字节或凭据交给 App Worker。
274
+
275
+ 附件只读当前 App 会话中用户提交的文件,不扩展自定义 Tool 的权限。新 App 业务 Tool 仍在 agent 目录用 `.tools.ts` 合并定义并由 Integration Runtime 执行。元数据 Schema 必须为静态字面量,execute 可导入项目自己的业务校验函数。
276
+
277
+ 真实表单协作可参考 `built-in-apps/opportunity-quotes`:页面以 SDK 布局并列 Form 与 Conversation,callback 接收建议,用户确认后更新本地草稿;正式保存沿原入口。NativeDOM 内仍只允许原有控件,不直接放 Conversation;可以在 Conversation 内用独立 NativeDOM 展示本地建议确认区。
278
+
279
+ ### Slider(源码增量,需匹配 Host)
280
+
281
+ `@77-zhike/app-sdk/ui` 提供单值/双端 `Slider`,支持整段拖动、step、minRange、键盘与最终commit。App只消费数值;Host合并在途变化并保留手势草稿。需先发布匹配的SDK和桌面/手机Host,再发布消费App。完整属性与行为见 Developer Docs `app-sdk/components/slider`。
package/package.json CHANGED
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "name": "@77-zhike/app-sdk",
3
- "version": "0.2.0-d7b1.2",
4
- "description": "\u667a\u5ba2 Integration App \u4e00\u671f SDK \u4e0e\u672c\u5730 Toolchain",
3
+ "version": "0.2.0-d7b1.20",
4
+ "description": "智客 Integration App 一期 SDK 与本地 Toolchain",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/tonyjiang88215/TruthLoop.git",
8
+ "directory": "developer-tools/packages/app-sdk"
9
+ },
5
10
  "type": "module",
6
11
  "engines": {
7
12
  "node": ">=20"
@@ -31,6 +36,14 @@
31
36
  "./forms": {
32
37
  "types": "./src/forms/index.d.ts",
33
38
  "default": "./src/forms/index.js"
39
+ },
40
+ "./jsx-runtime": {
41
+ "types": "./src/jsx-runtime.d.ts",
42
+ "default": "./src/jsx-runtime.js"
43
+ },
44
+ "./jsx-dev-runtime": {
45
+ "types": "./src/jsx-dev-runtime.d.ts",
46
+ "default": "./src/jsx-dev-runtime.js"
34
47
  }
35
48
  },
36
49
  "files": [
@@ -53,5 +66,6 @@
53
66
  "react": {
54
67
  "optional": true
55
68
  }
56
- }
69
+ },
70
+ "gitHead": "c7f1b793276cbde656dbd058731fcb8b51b95745"
57
71
  }
@@ -46,6 +46,10 @@ export class DeveloperApiClient {
46
46
  return this.request("/api/developer/organizations");
47
47
  }
48
48
 
49
+ async queryAppLogs(appId, query) {
50
+ return this.request(`/api/developer/cli/apps/${encodeURIComponent(appId)}/logs?${new URLSearchParams(query)}`);
51
+ }
52
+
49
53
  async getAppBySlug(organizationId, slug) {
50
54
  const path = `/api/developer/organizations/${encodeURIComponent(
51
55
  organizationId,
@@ -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",
@@ -32,9 +32,12 @@ export function formatAppLog(app, entry, {
32
32
  const levelLabel = { log: "INFO ", warn: "WARN ", error: "ERROR" }[level];
33
33
  const levelColor = { log: colors.cyan, warn: colors.yellow, error: colors.red }[level];
34
34
  const timestamp = formatTimestamp(entry.received_at);
35
+ const version = entry.app_version_id ? safe(entry.app_version_id).replace(/\n/g, " ").slice(0, 64) : "";
36
+ const request = entry.invocation_id ? safe(entry.invocation_id).replace(/\n/g, " ").slice(0, 64) : "";
37
+ const identity = version || request ? ` [${[version, request].filter(Boolean).join(" · ")}]` : "";
35
38
  const prefix = color
36
- ? `${colors.dim}${timestamp}${colors.reset} ${levelColor}${levelLabel}${colors.reset} ${colors.bold}${label}${colors.reset}`
37
- : `${timestamp} ${levelLabel} ${label}`;
39
+ ? `${colors.dim}${timestamp}${colors.reset} ${levelColor}${levelLabel}${colors.reset} ${colors.bold}${label}${colors.reset}${identity}`
40
+ : `${timestamp} ${levelLabel} ${label}${identity}`;
38
41
  return safe(entry.message).split("\n").map((line) => `${prefix} ${line}`).join("\n");
39
42
  }
40
43