@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68
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.en.md +86 -241
- package/README.ja.md +86 -241
- package/README.md +86 -241
- package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
- package/dist/chunk-FI56GONY.js.map +1 -0
- package/dist/chunk-MBPB37FR.js +1348 -0
- package/dist/chunk-MBPB37FR.js.map +1 -0
- package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
- package/dist/chunk-ZFNBUF77.js.map +1 -0
- package/dist/cli-config-templates-W2LLPMB6.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
- package/dist/index.js +1304 -532
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-DSJJXM2F.js +3849 -0
- package/dist/src-DSJJXM2F.js.map +1 -0
- package/package.json +9 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/messages/ja-JP.json +38 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +6 -1
- package/templates/apps/api/playwright.config.ts.hbs +15 -7
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
- package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/messages/ja-JP.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +23 -8
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +11 -3
- package/templates/base/playwright.config.ts +15 -4
- package/templates/base/src/hooks.server.ts.hbs +15 -6
- package/templates/base/src/lib/server/init.ts.hbs +139 -30
- package/templates/base/vite.config.ts.hbs +5 -3
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
- package/templates/i18n/project.inlang/settings.json +1 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/run-e2e.mjs +51 -12
- package/templates/skills/AGENTS.md +7 -3
- package/templates/skills/bridges/admin/AGENTS.md +6 -2
- package/templates/skills/bridges/api/AGENTS.md +3 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
- package/templates/skills/bridges/h5/AGENTS.md +6 -2
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
- package/templates/skills/bridges/website/AGENTS.md +6 -2
- package/templates/skills/hai-ai/SKILL.md +8 -2
- package/templates/skills/hai-ai/guide.md +134 -60
- package/templates/skills/hai-ai/reference.md +51 -11
- package/templates/skills/hai-api-client/SKILL.md +15 -160
- package/templates/skills/hai-api-client/reference.md +178 -0
- package/templates/skills/hai-api-contract/SKILL.md +16 -273
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +6 -2
- package/templates/skills/hai-app-create/development.md +24 -8
- package/templates/skills/hai-app-create/reference.md +7 -5
- package/templates/skills/hai-app-review/SKILL.md +7 -3
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +6 -3
- package/templates/skills/hai-app-tests/reference.md +5 -5
- package/templates/skills/hai-app-tests/validation.md +38 -0
- package/templates/skills/hai-audit/SKILL.md +24 -147
- package/templates/skills/hai-audit/reference.md +148 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +16 -13
- package/templates/skills/hai-cache/SKILL.md +20 -181
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +7 -4
- package/templates/skills/hai-capacitor/reference.md +15 -9
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +7 -3
- package/templates/skills/hai-core/reference.md +25 -5
- package/templates/skills/hai-crypto/SKILL.md +22 -250
- package/templates/skills/hai-crypto/reference.md +310 -0
- package/templates/skills/hai-datapipe/SKILL.md +16 -195
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +24 -54
- package/templates/skills/hai-deploy/middleware.md +51 -43
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +25 -11
- package/templates/skills/hai-iam/reference.md +181 -154
- package/templates/skills/hai-kit/SKILL.md +35 -174
- package/templates/skills/hai-kit/reference.md +190 -0
- package/templates/skills/hai-payment/SKILL.md +33 -263
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +21 -88
- package/templates/skills/hai-reach/SKILL.md +6 -3
- package/templates/skills/hai-reach/reference.md +83 -22
- package/templates/skills/hai-reldb/SKILL.md +6 -3
- package/templates/skills/hai-reldb/reference.md +68 -25
- package/templates/skills/hai-scheduler/SKILL.md +18 -195
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +25 -10
- package/templates/skills/hai-serv/reference.md +123 -87
- package/templates/skills/hai-spec-implement/SKILL.md +5 -1
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +5 -0
- package/templates/skills/hai-spec-implement/workflow.md +5 -3
- package/templates/skills/hai-spec-review/SKILL.md +4 -0
- package/templates/skills/hai-storage/SKILL.md +23 -238
- package/templates/skills/hai-storage/reference.md +282 -0
- package/templates/skills/hai-ui/SKILL.md +17 -3
- package/templates/skills/hai-ui/guide.md +3 -3
- package/templates/skills/hai-ui/reference.md +26 -11
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +26 -0
- package/templates/skills/hai-ui-review/SKILL.md +30 -0
- package/templates/skills/hai-ui-review/checklist.md +22 -0
- package/templates/skills/hai-vecdb/SKILL.md +14 -10
- package/templates/skills/hai-vecdb/reference.md +33 -3
- package/dist/chunk-FWN5B54L.js +0 -468
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-cache
|
|
3
|
-
description: "
|
|
3
|
+
description: "实现或排查 Memory/Redis 缓存、TTL、集合与分布式锁,处理过期、回源和并发边界。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-cache
|
|
@@ -11,192 +11,31 @@ description: "使用 @h-ai/cache 的 Memory/Redis 缓存、TTL、集合、排行
|
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| 能力 | 使用 @h-ai/cache 的 Memory/Redis 缓存、TTL、集合、排行榜和锁 |
|
|
13
13
|
| 适用场景 | 缓存读写、过期策略或共享资源互斥 |
|
|
14
|
-
| 输入 | CacheConfigInput、key/value、TTL
|
|
15
|
-
| 输出 | 缓存和锁操作的 HaiResult,获锁 data
|
|
14
|
+
| 输入 | CacheConfigInput、key/value、TTL;acquire 返回本次获锁唯一 owner |
|
|
15
|
+
| 输出 | 缓存和锁操作的 HaiResult,获锁 data 为唯一 owner 或 null |
|
|
16
16
|
| 限制 | 仅服务端;Memory 不跨节点。release/extend 携带本次 owner,避免锁过期后误操作新持有者。 |
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 使用路径
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
1. 先确定单进程 Memory 还是共享 Redis;配置校验与 init 成功后才使用。cache 不依赖 reldb。
|
|
21
|
+
2. 根据 KV/Hash/List/Set/ZSet 选择公开操作,明确 TTL、空值和回源失败的业务含义。
|
|
22
|
+
3. 读取 acquire 成功返回的唯一 owner,检查 success 与 data;续期/释放复用该 owner,丢锁后停止受保护工作。
|
|
23
|
+
4. 验证过期、连接失败、并发与锁过期后的旧任务;最后 close。
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
Cluster 的 mset/集合运算/多键 blocking pop 须同 slot;mget/del/exists 可跨 slot,但无全局原子性。scan 使用字符串游标遍历全部主节点,返回去除 keyPrefix 的逻辑 key;拓扑/条件变化后重新遍历。
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
## 按需参考
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
按本次操作读取对应小节;以已安装版本的公开类型核验签名,示例中的业务变量由应用提供。
|
|
27
30
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
31
|
+
- [运行环境](reference.md#运行环境)
|
|
32
|
+
- [适用场景](reference.md#适用场景)
|
|
33
|
+
- [使用步骤](reference.md#使用步骤)
|
|
34
|
+
- [核心 API](reference.md#核心-api)
|
|
35
|
+
- [错误码 — `HaiCacheError`](reference.md#错误码--haicacheerror)
|
|
36
|
+
- [常见模式](reference.md#常见模式)
|
|
37
|
+
- [相关 Skills](reference.md#相关-skills)
|
|
34
38
|
|
|
35
|
-
|
|
39
|
+
单键 compareAndSwap(key, expected, value, {px?,keepTtl?}) 比较 JSON 序列化快照并原子替换,必须检查 success 与 boolean data。expected=undefined 表示不存在,value=undefined 表示删除,null 是值;px 正整数且与 keepTtl 互斥。比较失败不修改 TTL;跨节点版本状态优先使用 CAS 而不是租约后无条件 SET。
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```yaml
|
|
40
|
-
# config/_cache.yml
|
|
41
|
-
type: memory # memory | redis
|
|
42
|
-
# Redis 配置(type=redis 时生效):
|
|
43
|
-
# host: localhost
|
|
44
|
-
# port: 6379
|
|
45
|
-
# password: ''
|
|
46
|
-
# db: 0
|
|
47
|
-
# keyPrefix: 'hai:'
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
约定环境变量优先于 YAML,例如 `HAI_CACHE_TYPE`、`HAI_CACHE_HOST`、
|
|
51
|
-
`HAI_CACHE_KEYPREFIX`。
|
|
52
|
-
|
|
53
|
-
### 2. 初始化与关闭
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { cache } from '@h-ai/cache'
|
|
57
|
-
|
|
58
|
-
await cache.init(core.config.get('cache'))
|
|
59
|
-
// 使用后关闭
|
|
60
|
-
await cache.close()
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
`cache.config` 返回的是脱敏后的配置快照;Redis `password` / `url` 等敏感值不会原样暴露给日志或调试输出。
|
|
64
|
-
|
|
65
|
-
## 核心 API
|
|
66
|
-
|
|
67
|
-
### KV 操作(`cache.kv`)
|
|
68
|
-
|
|
69
|
-
| 方法 | 说明 |
|
|
70
|
-
| ----------------------------------- | -------------------- |
|
|
71
|
-
| `get / set / del / exists` | 基础读写与存在性判断 |
|
|
72
|
-
| `expire / expireAt / ttl / persist` | TTL 管理 |
|
|
73
|
-
| `incr / incrBy / decr / decrBy` | 计数器操作 |
|
|
74
|
-
| `mget / mset` | 批量读写 |
|
|
75
|
-
| `scan / keys / type` | Key 检索与类型判断 |
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
await cache.kv.set('user:123', { name: '张三' }, { ex: 3600 })
|
|
79
|
-
const result = await cache.kv.get<{ name: string }>('user:123')
|
|
80
|
-
if (result.success && result.data) {
|
|
81
|
-
// result.data.name === '张三'
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await cache.kv.del('user:123')
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Hash/List/Set/ZSet(`cache.hash/list/set_/zset`)
|
|
88
|
-
|
|
89
|
-
- `cache.hash`:对象字段读写(如用户 profile 局部更新)
|
|
90
|
-
- `cache.list`:队列/消息顺序处理
|
|
91
|
-
- `cache.set_`:去重集合(成员关系、权限集合)
|
|
92
|
-
- `cache.zset`:分数排序(排行榜、权重调度)
|
|
93
|
-
|
|
94
|
-
```typescript
|
|
95
|
-
await cache.hash.hset('profile:1', { nickname: 'alice' })
|
|
96
|
-
await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
|
|
97
|
-
await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
|
|
98
|
-
await cache.zset.zadd('rank:daily', { member: 'u1', score: 100 })
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
### 分布式锁(`cache.lock`)
|
|
102
|
-
|
|
103
|
-
| 方法 | 说明 |
|
|
104
|
-
| ---------- | ------------------------------------------ |
|
|
105
|
-
| `acquire` | 尝试获锁(SET NX EX),返回 true/false |
|
|
106
|
-
| `release` | 释放锁(支持 owner 验证,防止误释放) |
|
|
107
|
-
| `isLocked` | 检查锁是否被持有 |
|
|
108
|
-
| `extend` | 续期锁 TTL(支持 owner 验证) |
|
|
109
|
-
|
|
110
|
-
```typescript
|
|
111
|
-
// 每次尝试生成唯一 owner,后续释放/续期复用同一个值
|
|
112
|
-
const owner = core.id.generate()
|
|
113
|
-
const acquired = await cache.lock.acquire('my-lock', { ttl: 30, owner })
|
|
114
|
-
if (acquired.success && acquired.data) {
|
|
115
|
-
try {
|
|
116
|
-
// 长任务仅在仍持有锁时续期;失败或 data=false 时停止依赖锁的后续操作
|
|
117
|
-
const extended = await cache.lock.extend('my-lock', 60, owner)
|
|
118
|
-
if (!extended.success || !extended.data) {
|
|
119
|
-
logger.warn('Lock extension failed; protected work cancelled')
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
// 受保护的操作
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
finally {
|
|
126
|
-
await cache.lock.release('my-lock', owner)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 检查
|
|
131
|
-
const locked = await cache.lock.isLocked('my-lock')
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
**最佳实践:**
|
|
135
|
-
|
|
136
|
-
- `owner` 每次获锁尝试唯一,释放/续期复用该值;仅用 nodeId 会使同节点旧任务误释放过期后重新获得的锁
|
|
137
|
-
- 释放锁时传入 `owner` 防止误释放他人锁
|
|
138
|
-
- Memory 后端适合开发/测试;Redis 后端用 Lua 脚本保证 release/extend 原子性
|
|
139
|
-
|
|
140
|
-
## 错误码 — `HaiCacheError`
|
|
141
|
-
|
|
142
|
-
| 错误码 | code | 说明 |
|
|
143
|
-
|--------|------|------|
|
|
144
|
-
| `HaiCacheError.CONNECTION_FAILED` | `hai:cache:001` | 连接失败 |
|
|
145
|
-
| `HaiCacheError.OPERATION_FAILED` | `hai:cache:002` | 操作失败 |
|
|
146
|
-
| `HaiCacheError.SERIALIZATION_FAILED` | `hai:cache:003` | 序列化失败 |
|
|
147
|
-
| `HaiCacheError.DESERIALIZATION_FAILED` | `hai:cache:004` | 反序列化失败 |
|
|
148
|
-
| `HaiCacheError.KEY_NOT_FOUND` | `hai:cache:005` | 键不存在 |
|
|
149
|
-
| `HaiCacheError.TIMEOUT` | `hai:cache:006` | 超时 |
|
|
150
|
-
| `HaiCacheError.NOT_INITIALIZED` | `hai:cache:010` | 未初始化 |
|
|
151
|
-
| `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 不支持的缓存类型 |
|
|
152
|
-
| `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 配置错误 |
|
|
153
|
-
|
|
154
|
-
## 常见模式
|
|
155
|
-
|
|
156
|
-
### 缓存命中与回源(未缓存空值,不防穿透)
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
async function getUserCached(userId: string) {
|
|
160
|
-
const cached = await cache.kv.get<User>(`user:${userId}`)
|
|
161
|
-
if (cached.success && cached.data)
|
|
162
|
-
return cached.data
|
|
163
|
-
|
|
164
|
-
const user = await userRepo.findById(userId)
|
|
165
|
-
if (user.success && user.data) {
|
|
166
|
-
await cache.kv.set(`user:${userId}`, user.data, { ex: 3600 })
|
|
167
|
-
return user.data
|
|
168
|
-
}
|
|
169
|
-
return null
|
|
170
|
-
}
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
### 分布式锁保护共享资源
|
|
174
|
-
|
|
175
|
-
```typescript
|
|
176
|
-
const lockKey = 'batch:import'
|
|
177
|
-
const owner = core.id.generate()
|
|
178
|
-
const acquired = await cache.lock.acquire(lockKey, { ttl: 60, owner })
|
|
179
|
-
if (acquired.success && acquired.data) {
|
|
180
|
-
try {
|
|
181
|
-
await runBatchImport()
|
|
182
|
-
}
|
|
183
|
-
finally {
|
|
184
|
-
await cache.lock.release(lockKey, owner)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
else if (acquired.success) {
|
|
188
|
-
logger.info('Another node is running the import')
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
logger.error('Lock acquisition failed', { code: acquired.error.code })
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 相关 Skills
|
|
196
|
-
|
|
197
|
-
- `hai-build`:模块初始化顺序(cache 在 db 之后、iam 之前)
|
|
198
|
-
- `hai-core`:配置与 HaiResult 模型
|
|
199
|
-
- `hai-iam`:会话存储与权限缓存(底层使用 cache)
|
|
200
|
-
- `hai-scheduler`:定时任务分布式锁(底层使用 cache.lock)
|
|
201
|
-
- `hai-reach`:消息发送互斥锁(底层使用 cache.lock)
|
|
202
|
-
- `hai-kit`:SvelteKit 集成层
|
|
41
|
+
`kv.set` 返回 `HaiResult<boolean>`:`data=true` 表示实际写入,NX/XX 未命中返回 `success=true,data=false` 且不改变原值或 TTL。签发票据、会话或登记公钥前必须检查这两个字段。`ex/px/exat/pxat` 分别使用秒、毫秒、Unix 秒、Unix 毫秒,须为正安全整数且只能指定一种,与 `keepTtl=true` 互斥;`nx/xx` 也互斥。零、负数、小数、非有限值或溢出返回 `INVALID_SET_OPTIONS`(`cache:016`),不会写成永久键。过去的正绝对时间表示立即过期。省略期限会移除原 TTL;`keepTtl` 保留已有 TTL,新键仍永久保存。memory 和 Redis 都保存 JSON 快照。
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# hai-cache 参考
|
|
2
|
+
|
|
3
|
+
> `@h-ai/cache` 提供统一缓存接口,支持 Memory 与 Redis 后端,包含 KV / Hash / List / Set / ZSet / 分布式锁 六类操作。
|
|
4
|
+
|
|
5
|
+
内存 KV 在到期时间点即失效。整数计数器拒绝 null/boolean/array、非整数增量和安全整数溢出,失败不修改原值;Hash 的 `__proto__` / `constructor` 名称按普通字段保留。
|
|
6
|
+
|
|
7
|
+
## 运行环境
|
|
8
|
+
|
|
9
|
+
> ⚠️ **服务端模块(Node.js only)。** 浏览器端无需直接操作缓存,由服务端模块(如 IAM、Scheduler)内部使用。
|
|
10
|
+
|
|
11
|
+
## 适用场景
|
|
12
|
+
|
|
13
|
+
- 缓存热点数据减少数据库查询
|
|
14
|
+
- 会话存储(配合 IAM)
|
|
15
|
+
- 分布式缓存(Redis)与本地测试缓存(Memory)
|
|
16
|
+
- 集合操作(权限缓存、标签集合)
|
|
17
|
+
- 排行榜(ZSet)与队列(List)
|
|
18
|
+
- 分布式锁(多节点部署互斥控制)
|
|
19
|
+
|
|
20
|
+
## 使用步骤
|
|
21
|
+
|
|
22
|
+
### 1. 配置
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
# config/_cache.yml
|
|
26
|
+
type: memory # memory | redis
|
|
27
|
+
# Redis 配置(type=redis 时生效):
|
|
28
|
+
# host: localhost
|
|
29
|
+
# port: 6379
|
|
30
|
+
# password: ''
|
|
31
|
+
# db: 0
|
|
32
|
+
# keyPrefix: 'hai:'
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
约定环境变量优先于 YAML,例如 `HAI_CACHE_TYPE`、`HAI_CACHE_HOST`、
|
|
36
|
+
`HAI_CACHE_KEYPREFIX`。
|
|
37
|
+
|
|
38
|
+
### 2. 初始化与关闭
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { cache } from '@h-ai/cache'
|
|
42
|
+
|
|
43
|
+
await cache.init(core.config.get('cache'))
|
|
44
|
+
// 使用后关闭
|
|
45
|
+
const closed = await cache.close()
|
|
46
|
+
if (!closed.success) throw new Error(closed.error.message)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
初始化只在健康检查成功后发布连接;失败时释放候选资源。若清理仍失败,初始化错误的 `error.ext.cleanupPending` 为 `true`。`cache.close()` 返回 `Promise<HaiResult<void>>`,等待正在进行的初始化,并合并并发关闭。开始清理后 `isInitialized` 为 `false`,配置与业务操作不再可用;清理失败仍保留资源,后续 `close()` 或 `init()` 先重试释放,成功前不会创建替代连接。调用方必须检查关闭结果。
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const closed = await cache.close()
|
|
54
|
+
if (!closed.success) {
|
|
55
|
+
// 处理清理故障后有限重试;再次失败应向应用报告。
|
|
56
|
+
const retried = await cache.close()
|
|
57
|
+
if (!retried.success) throw new Error(retried.error.message)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
`cache.config` 返回的是脱敏后的配置快照;Redis `password` / `url` 等敏感值不会原样暴露给日志或调试输出。
|
|
63
|
+
|
|
64
|
+
## 核心 API
|
|
65
|
+
|
|
66
|
+
### KV 操作(`cache.kv`)
|
|
67
|
+
|
|
68
|
+
| 方法 | 说明 |
|
|
69
|
+
| ----------------------------------- | -------------------- |
|
|
70
|
+
| `get / set / del / exists` | 基础读写与存在性判断 |
|
|
71
|
+
| `expire / expireAt / ttl / persist` | TTL 管理 |
|
|
72
|
+
| `incr / incrBy / decr / decrBy` | 计数器操作 |
|
|
73
|
+
| `mget / mset` | 批量读写 |
|
|
74
|
+
| `scan / keys / type` | Key 检索与类型判断 |
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
await cache.kv.set('user:123', { name: '张三' }, { ex: 3600 })
|
|
78
|
+
const result = await cache.kv.get<{ name: string }>('user:123')
|
|
79
|
+
if (result.success && result.data) {
|
|
80
|
+
// result.data.name === '张三'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await cache.kv.del('user:123')
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Cluster 多键与扫描
|
|
87
|
+
|
|
88
|
+
`mget/del/exists` 跨 slot 时有界并发且无整体原子性/快照;mget 保留输入顺序,DEL 失败可能已部分删除,重试幂等。`mset`、集合交/并/差和多键 blocking pop 须同 slot,否则返回 `CROSS_SLOT`;使用共同的 `{hash-tag}`,MSET 不做部分写入回退。
|
|
89
|
+
|
|
90
|
+
scan 首次传字符串 `'0'`,后续原样传回不透明游标,返回 `'0'` 表示完成。Cluster 覆盖所有 master;同配置和拓扑的另一应用节点可接续。拓扑、match 或 keyPrefix 改变时返回 `SCAN_TOPOLOGY_CHANGED`,从 `'0'` 重启;无效游标或 count 返回 `INVALID_SCAN`。返回去除 keyPrefix 的逻辑 key,前缀 glob 字符按字面量解释;keys 同样扫描并去重。SCAN 无快照,可能重复,count 仅为正整数提示值。
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const first = await cache.kv.scan('0', { match: 'users:*', count: 100 })
|
|
94
|
+
if (first.success && first.data[0] !== '0') {
|
|
95
|
+
const next = await cache.kv.scan(first.data[0], { match: 'users:*', count: 100 })
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Hash/List/Set/ZSet(`cache.hash/list/set_/zset`)
|
|
100
|
+
|
|
101
|
+
- `cache.hash`:对象字段读写(如用户 profile 局部更新)
|
|
102
|
+
- `cache.list`:队列/消息顺序处理
|
|
103
|
+
- `cache.set_`:去重集合(成员关系、权限集合)
|
|
104
|
+
- `cache.zset`:分数排序(排行榜、权重调度)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
await cache.hash.hset('profile:1', { nickname: 'alice' })
|
|
108
|
+
await cache.list.lpush('queue:jobs', 'job-1', 'job-2')
|
|
109
|
+
await cache.set_.sadd('role:admin:perms', 'user.read', 'user.write')
|
|
110
|
+
await cache.zset.zadd('rank:daily', { member: 'u1', score: 100 })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 分布式锁(`cache.lock`)
|
|
114
|
+
|
|
115
|
+
| 方法 | 说明 |
|
|
116
|
+
| ---------- | ------------------------------------------ |
|
|
117
|
+
| `acquire` | 尝试获锁(SET NX EX),返回唯一 owner 或 null |
|
|
118
|
+
| `release` | 释放锁(必须携带本次 owner,防止误释放) |
|
|
119
|
+
| `isLocked` | 检查锁是否被持有 |
|
|
120
|
+
| `extend` | 续期锁 TTL(必须携带本次 owner) |
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// acquire 返回本次唯一 owner,后续释放/续期必须复用该凭据
|
|
124
|
+
const acquired = await cache.lock.acquire('my-lock', { ttl: 30 })
|
|
125
|
+
if (acquired.success && acquired.data) {
|
|
126
|
+
const owner = acquired.data
|
|
127
|
+
try {
|
|
128
|
+
// 长任务仅在仍持有锁时续期;失败或 data=false 时停止依赖锁的后续操作
|
|
129
|
+
const extended = await cache.lock.extend('my-lock', 60, owner)
|
|
130
|
+
if (!extended.success || !extended.data) {
|
|
131
|
+
logger.warn('Lock extension failed; protected work cancelled')
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
// 受保护的操作
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await cache.lock.release('my-lock', owner)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 检查
|
|
143
|
+
const locked = await cache.lock.isLocked('my-lock')
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**最佳实践:**
|
|
147
|
+
|
|
148
|
+
- `owner` 必须取自成功的 acquire.data;缓存内部每次生成唯一凭据,不能使用 nodeId 替代。释放/续期复用该凭据。
|
|
149
|
+
- 释放锁时传入 `owner` 防止误释放他人锁
|
|
150
|
+
- Memory 后端适合开发/测试;Redis 后端用 Lua 脚本保证 release/extend 原子性
|
|
151
|
+
|
|
152
|
+
## 错误码 — `HaiCacheError`
|
|
153
|
+
|
|
154
|
+
| 错误码 | code | 说明 |
|
|
155
|
+
|--------|------|------|
|
|
156
|
+
| `HaiCacheError.CONNECTION_FAILED` | `hai:cache:001` | 连接失败 |
|
|
157
|
+
| `HaiCacheError.OPERATION_FAILED` | `hai:cache:002` | 操作失败 |
|
|
158
|
+
| `HaiCacheError.SERIALIZATION_FAILED` | `hai:cache:003` | 序列化失败 |
|
|
159
|
+
| `HaiCacheError.DESERIALIZATION_FAILED` | `hai:cache:004` | 反序列化失败 |
|
|
160
|
+
| `HaiCacheError.KEY_NOT_FOUND` | `hai:cache:005` | 键不存在 |
|
|
161
|
+
| `HaiCacheError.TIMEOUT` | `hai:cache:006` | 超时 |
|
|
162
|
+
| `HaiCacheError.NOT_INITIALIZED` | `hai:cache:010` | 未初始化 |
|
|
163
|
+
| `HaiCacheError.UNSUPPORTED_TYPE` | `hai:cache:011` | 不支持的缓存类型 |
|
|
164
|
+
| `HaiCacheError.CONFIG_ERROR` | `hai:cache:012` | 配置错误 |
|
|
165
|
+
| `HaiCacheError.INVALID_SCAN` | `hai:cache:013` | 游标或 count 无效 |
|
|
166
|
+
| `HaiCacheError.SCAN_TOPOLOGY_CHANGED` | `hai:cache:014` | 拓扑或条件变化,重新遍历 |
|
|
167
|
+
| `HaiCacheError.CROSS_SLOT` | `hai:cache:015` | 多键操作不在同一 slot |
|
|
168
|
+
|
|
169
|
+
## 常见模式
|
|
170
|
+
|
|
171
|
+
### 缓存命中与回源(未缓存空值,不防穿透)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
async function getUserCached(userId: string) {
|
|
175
|
+
const cached = await cache.kv.get<User>(`user:${userId}`)
|
|
176
|
+
if (cached.success && cached.data)
|
|
177
|
+
return cached.data
|
|
178
|
+
|
|
179
|
+
const user = await userRepo.findById(userId)
|
|
180
|
+
if (user.success && user.data) {
|
|
181
|
+
await cache.kv.set(`user:${userId}`, user.data, { ex: 3600 })
|
|
182
|
+
return user.data
|
|
183
|
+
}
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 分布式锁保护共享资源
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const lockKey = 'batch:import'
|
|
192
|
+
const acquired = await cache.lock.acquire(lockKey, { ttl: 60 })
|
|
193
|
+
if (acquired.success && acquired.data) {
|
|
194
|
+
const owner = acquired.data
|
|
195
|
+
try {
|
|
196
|
+
await runBatchImportWithConditionalCommit()
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await cache.lock.release(lockKey, owner)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (acquired.success) {
|
|
203
|
+
logger.info('Another node is running the import')
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
logger.error('Lock acquisition failed', { code: acquired.error.code })
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## 相关 Skills
|
|
211
|
+
|
|
212
|
+
- `hai-build`:模块初始化顺序(cache 在 iam 之前;cache 自身不依赖 reldb)
|
|
213
|
+
- `hai-core`:配置与 HaiResult 模型
|
|
214
|
+
- `hai-iam`:会话存储与权限缓存(底层使用 cache)
|
|
215
|
+
- `hai-scheduler`:定时任务分布式锁(底层使用 cache.lock)
|
|
216
|
+
- `hai-reach`:消息发送互斥锁(底层使用 cache.lock)
|
|
217
|
+
- `hai-kit`:SvelteKit 集成层
|
|
218
|
+
|
|
219
|
+
Cluster 与 Sentinel 数据节点认证使用 `password`,Cluster 发现的新节点继承同一鉴权配置。`url` 模式只使用 URL 内凭据,单独 password 不覆盖 URL。
|
|
220
|
+
|
|
221
|
+
单键 compareAndSwap(key, expected, value, {px?,keepTtl?}) 比较 JSON 序列化快照并原子替换,必须检查 success 与 boolean data。expected=undefined 表示不存在,value=undefined 表示删除,null 是值;px 正整数且与 keepTtl 互斥。比较失败不修改 TTL;跨节点版本状态优先使用 CAS 而不是租约后无条件 SET。
|
|
222
|
+
|
|
223
|
+
owner 只校验租约续期/释放,不是资源 fencing token。暂停或过期后旧任务仍可能执行,最终持久化写入必须使用资源自己的 CAS/代际条件/事务约束。批量导入等应用函数须在每次提交时校验条件,不能只靠锁包裹 Promise。
|
|
224
|
+
|
|
225
|
+
## 生产共享状态
|
|
226
|
+
|
|
227
|
+
生产仅使用 Redis(含 Cluster);memory 只用于开发/明确测试。Redis 故障返回失败,不回退本机缓存。 NODE_ENV 或 HAI_CORE_ENV 为 production 时在初始化成功前检查,只有 HAI_E2E=1/HAI_E2E_MOCK=1 明确测试豁免。不要将租约当作最终写入条件,外部未知效果须按稳定业务键核对。
|
|
228
|
+
|
|
229
|
+
`kv.set` 返回 `HaiResult<boolean>`:`data=true` 表示实际写入,NX/XX 未命中返回 `success=true,data=false` 且不改变原值或 TTL。签发票据、会话或登记公钥前必须检查这两个字段。`ex/px/exat/pxat` 分别使用秒、毫秒、Unix 秒、Unix 毫秒,须为正安全整数且只能指定一种,与 `keepTtl=true` 互斥;`nx/xx` 也互斥。零、负数、小数、非有限值或溢出返回 `INVALID_SET_OPTIONS`(`cache:016`),不会写成永久键。过去的正绝对时间表示立即过期。省略期限会移除原 TTL;`keepTtl` 保留已有 TTL,新键仍永久保存。memory 和 Redis 都保存 JSON 快照。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-capacitor
|
|
3
|
-
description: "
|
|
3
|
+
description: "在 Android/iOS 应用中接入 @h-ai/capacitor 设备、相机、推送与安全 Token 存储,处理 Web/原生差异。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-capacitor
|
|
@@ -13,16 +13,17 @@ description: "使用 @h-ai/capacitor 接入原生设备、相机、推送、状
|
|
|
13
13
|
| 适用场景 | Android/iOS 原生壳集成与平台差异处理 |
|
|
14
14
|
| 输入 | Capacitor 配置、原生插件、设备权限和平台状态 |
|
|
15
15
|
| 输出 | 原生操作结果及 TokenStorage 适配器 |
|
|
16
|
-
| 限制 | 区分浏览器与 Android/iOS;安全存储需原生插件,Web
|
|
16
|
+
| 限制 | 区分浏览器与 Android/iOS;安全存储需原生插件,Web 使用 httpOnly Cookie/内存认证,不降级到 localStorage。H5 验证不能代替真机验证。 |
|
|
17
17
|
|
|
18
18
|
## 使用路径
|
|
19
19
|
|
|
20
20
|
- 先判断平台和插件可用性,复用已有 SPA 入口;原生 UI 变化验证权限拒绝、取消和返回路径。
|
|
21
|
-
- TokenStorage 在原生端使用安全插件;Web
|
|
21
|
+
- TokenStorage 在原生端使用安全插件;Web/插件不可用或存储失败时拒绝 Promise。调用方处理 setTokens/clear 失败并释放 loading;clear 的实例内屏蔽不替代服务端撤销。
|
|
22
|
+
- 原生打包前确认公开 API 地址的运行配置入口;fullstack App 使用 `public/runtime-config.js`,构建不会读取部署环境文件。这里只放公开地址,不能放 Token 或密钥。
|
|
22
23
|
|
|
23
24
|
## 按需参考
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
|
|
26
27
|
|
|
27
28
|
- 运行环境
|
|
28
29
|
- 适用场景
|
|
@@ -32,3 +33,5 @@ description: "使用 @h-ai/capacitor 接入原生设备、相机、推送、状
|
|
|
32
33
|
- 常见模式
|
|
33
34
|
- 插件依赖
|
|
34
35
|
- 相关 Skills
|
|
36
|
+
|
|
37
|
+
推送仅在实际原生平台调用插件。`push.register({ signal? })` 与 `push.listen(callbacks, { signal? })` 的设置阶段限时 30 秒(包含权限和监听器挂载);失败、超时、取消及 `capacitor.close()` 都会停止回调并移除本次拥有的句柄,迟到的句柄在完成后立即移除,不调用影响其他消费者的 removeAllListeners。listen 成功后信号仍有效,也可调用返回的 cleanup。cleanup 与 close 均返回 `HaiResult<void>`;原生 remove 失败返回 `PUSH_CLEANUP_FAILED`(`capacitor:032`),失败句柄保留供再次调用重试,成功移除的句柄不重复操作。模块关闭后立即不可用;重新初始化前先重试遗留清理。自动化验证控制插件时序,不能代替 Android/iOS 真机 FCM/APNs 权限、注册与反复挂载验收。
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
从 [SKILL.md](SKILL.md) 选择主题后读取对应小节。代码片段展示 API 用法;省略的业务变量、schema、依赖与 return 上下文需由应用补齐。
|
|
4
4
|
|
|
5
|
-
> `@h-ai/capacitor` 是 hai-framework 的 Capacitor
|
|
5
|
+
> `@h-ai/capacitor` 是 hai-framework 的 Capacitor 原生桥接模块。设备、推送等领域操作返回 `HaiResult<T>`;TokenStorage 是直接 Promise 适配器,失败会拒绝。与 `@h-ai/api-client` 配合时,Token 仅存于原生安全存储,不回退到 Web `localStorage`。
|
|
6
6
|
|
|
7
7
|
## 运行环境
|
|
8
8
|
|
|
@@ -42,12 +42,14 @@ await capacitor.close()
|
|
|
42
42
|
|
|
43
43
|
### 2. Token 安全存储(与 api-client 配合)
|
|
44
44
|
|
|
45
|
+
先复用应用已有的 API 初始化入口。示例中的 `apiBase` 由应用解析公开运行配置,已经包含 API 前缀,不重复拼接 `/api/v1`。Web 部署可通过 `runtime-config.js` 覆盖地址;原生包在复制静态资源前设置公开配置。
|
|
46
|
+
|
|
45
47
|
```typescript
|
|
46
48
|
import { apiClient } from '@h-ai/api-client'
|
|
47
49
|
import { createCapacitorTokenStorage } from '@h-ai/capacitor'
|
|
48
50
|
|
|
49
51
|
await apiClient.init({
|
|
50
|
-
baseUrl:
|
|
52
|
+
baseUrl: apiBase,
|
|
51
53
|
auth: {
|
|
52
54
|
storage: createCapacitorTokenStorage(),
|
|
53
55
|
},
|
|
@@ -58,9 +60,11 @@ await apiClient.init({
|
|
|
58
60
|
|
|
59
61
|
- Android → Android KeyStore + 加密 SharedPreferences
|
|
60
62
|
- iOS → Keychain
|
|
61
|
-
- Web →
|
|
63
|
+
- Web → **不提供原生存储回退**;适配器方法拒绝 Promise,Web 入口选择 api-client 的 Cookie/内存存储
|
|
64
|
+
|
|
65
|
+
读取、保存与删除失败均拒绝 Promise。调用方通过 `apiClient.auth` 串行更新凭据并捕获 setTokens/clear 失败;界面释放 loading,退出失败仍清理内存身份并提示错误。`clear()` 开始立即屏蔽本实例两类旧凭据,等待两个删除都结束;即使失败也保持屏蔽,相应 key 成功写入后重新开放。屏蔽不等于持久删除,跨实例/重启撤销仍依赖服务端退出/撤销会话。插件恢复后可以在同一实例重试,不缓存永久失败状态。
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
任一 key 写入失败也会屏蔽整对凭据,避免 access 已落盘而 refresh 保存失败后的半完成登录。登录/注册保存失败后尝试 `auth.clear()` 清理部分数据;清理失败继续显示未确认删除的错误,不能只清界面身份或宣称持久删除成功。
|
|
64
68
|
|
|
65
69
|
#### Preferences 子操作
|
|
66
70
|
|
|
@@ -165,7 +169,7 @@ await capacitor.statusBar.show()
|
|
|
165
169
|
| API | 用途 | 返回值 |
|
|
166
170
|
| --- | --- | --- |
|
|
167
171
|
| `capacitor.init()` | 初始化模块(检测环境) | `HaiResult<void>` |
|
|
168
|
-
| `capacitor.close()` |
|
|
172
|
+
| `capacitor.close()` | 关闭模块并回收推送,失败可重试 | `Promise<HaiResult<void>>` |
|
|
169
173
|
| `capacitor.getPlatform()` | 获取当前平台 | `'android' \| 'ios' \| 'web'` |
|
|
170
174
|
| `capacitor.isNative()` | 是否为原生环境 | `boolean` |
|
|
171
175
|
| `capacitor.isInitialized` | 是否已初始化 | `boolean` |
|
|
@@ -176,7 +180,7 @@ await capacitor.statusBar.show()
|
|
|
176
180
|
| `capacitor.device.getInfo()` | 设备信息 | `HaiResult<DeviceInfo>` |
|
|
177
181
|
| `capacitor.device.getAppVersion()` | 应用版本 | `HaiResult<{ version, build }>` |
|
|
178
182
|
| `capacitor.push.register()` | 注册推送 | `HaiResult<PushRegistration>` |
|
|
179
|
-
| `capacitor.push.listen(callbacks)` | 监听推送事件 | `HaiResult<
|
|
183
|
+
| `capacitor.push.listen(callbacks)` | 监听推送事件 | `HaiResult<PushCleanup>`(cleanup 返回 `Promise<HaiResult<void>>`) |
|
|
180
184
|
| `capacitor.camera.takePhoto(options?)` | 拍照 / 选取图片 | `HaiResult<PhotoResult>` |
|
|
181
185
|
| `capacitor.statusBar.configure(config)` | 配置状态栏 | `HaiResult<void>` |
|
|
182
186
|
| `capacitor.statusBar.show()` | 显示状态栏 | `HaiResult<void>` |
|
|
@@ -236,7 +240,7 @@ export async function initCapacitor() {
|
|
|
236
240
|
|
|
237
241
|
### SPA 模式配置(必需)
|
|
238
242
|
|
|
239
|
-
Capacitor
|
|
243
|
+
Capacitor 原生壳读取 `webDir` 指定的静态产物。纯 Vite 模板默认为 `dist`;SvelteKit 项目需配置静态适配器,输出可能是 `build`。构建器、容器与 `webDir` 必须一致,以下是 Vite 示例:
|
|
240
244
|
|
|
241
245
|
```typescript
|
|
242
246
|
// capacitor.config.ts
|
|
@@ -258,9 +262,9 @@ export default config
|
|
|
258
262
|
import { apiClient } from '@h-ai/api-client'
|
|
259
263
|
import { createCapacitorTokenStorage } from '@h-ai/capacitor'
|
|
260
264
|
|
|
261
|
-
export async function initApi() {
|
|
265
|
+
export async function initApi(apiBase: string) {
|
|
262
266
|
return apiClient.init({
|
|
263
|
-
baseUrl:
|
|
267
|
+
baseUrl: apiBase,
|
|
264
268
|
auth: {
|
|
265
269
|
storage: createCapacitorTokenStorage(),
|
|
266
270
|
},
|
|
@@ -350,3 +354,5 @@ async function captureAndUpload() {
|
|
|
350
354
|
- `hai-ui`:移动端 UI 组件(SafeArea、BottomNav 等)
|
|
351
355
|
|
|
352
356
|
原生交付必须单独执行 Tauri/Gradle/Xcode 打包,并在目标 OS/设备验证安装启动、登录/退出、重启会话、安全 TokenStore、真实 API 域名与覆盖升级。Web 构建/E2E 不代替原生验收。保存提交、包 hash、平台/工具链及逐项结果;缺平台写未验证,禁止静默 skip。hai-framework 仓库参考 `.github/workflows/native.yml` 与 desktop-app / mobile-app README 的「原生交付与验收」章节。
|
|
357
|
+
|
|
358
|
+
推送仅在实际原生平台调用插件。`push.register({ signal? })` 与 `push.listen(callbacks, { signal? })` 的设置阶段限时 30 秒(包含权限和监听器挂载);失败、超时、取消及 `capacitor.close()` 都会停止回调并移除本次拥有的句柄,迟到的句柄在完成后立即移除,不调用影响其他消费者的 removeAllListeners。listen 成功后信号仍有效,也可调用返回的 cleanup。cleanup 与 close 均返回 `HaiResult<void>`;原生 remove 失败返回 `PUSH_CLEANUP_FAILED`(`capacitor:032`),失败句柄保留供再次调用重试,成功移除的句柄不重复操作。模块关闭后立即不可用;重新初始化前先重试遗留清理。自动化验证控制插件时序,不能代替 Android/iOS 真机 FCM/APNs 权限、注册与反复挂载验收。
|
|
@@ -52,6 +52,8 @@ pnpm test
|
|
|
52
52
|
|
|
53
53
|
从当前 package.json 确认实际 scripts,使用声明的最低 Node 版本验证并与 engines/README 同步;PowerShell 使用 `pnpm.cmd`。测试内容由 `hai-app-tests` 定义;本技能决定门禁如何稳定执行。
|
|
54
54
|
|
|
55
|
+
AI 本地验收和排查按 [分轮验证与集中修复](../hai-app-tests/validation.md) 收集所有可执行结果,再集中修复;普通失败不立即从头重跑。独立 CI job 可继续收集结果,真实前置失败则阻塞其依赖 job;必需检查与发布步骤仍须全部成功,不以继续收集为由设置 `continue-on-error` 或放行失败。
|
|
56
|
+
|
|
55
57
|
lint/typecheck 只能执行已配置的规则。新增开发规范时区分可静态检测项(如显式 any/console.log)与必须行为验收项(如资源越权、事务/幂等、失败结果、生命周期清理),后者在适用单元/集成/E2E 中验证。禁止通过关闭规则、添加无依据忽略或把 `|| true` 加到门禁来“统一风格”。
|
|
56
58
|
|
|
57
59
|
### E2E 策略
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-core
|
|
3
|
-
description: "
|
|
3
|
+
description: "配置 hai 应用启动、YAML/环境变量、日志、i18n 与 HaiResult;排查配置覆盖和错误本地化。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-core
|
|
@@ -18,15 +18,17 @@ description: "使用 @h-ai/core 配置、日志、i18n、ID、Zod 错误本地
|
|
|
18
18
|
## 使用路径
|
|
19
19
|
|
|
20
20
|
- Node 启动用 core.init({ configDir }) 加载配置;勿传 logging 覆盖 _core.yml 中的日志设置,除非任务明确要求。
|
|
21
|
-
-
|
|
21
|
+
- 自动加载和监听精确跳过 `_deploy.yml` / `_deploy.yaml`,部署 CLI 按目标环境和凭据独立解析;业务 `deploy.yml`、手动 `config.load` 不受影响。单项 load 返回失败时记日志并继续,必需配置须 validate 并检查 success;目录扫描、日志设置等异常仍可能传播。
|
|
22
|
+
- 应用名称、版本、环境、默认语言和日志设置统一从 `_core.yml` 经 core.config 获取,日志使用 core.logger;UI 不另维护硬编码副本。SvelteKit layout / fullstack app.info 仅下发公开字段白名单;独立 Vite mobile-app 在 Node 构建时解析并注入公开默认值,修改这些编译默认值需要重新构建。静态 Web 的公开 API 地址等通过 `runtime-config.js` 覆盖时无需重建镜像,原生包在复制静态资源前准备配置入口。浏览器不加载 YAML,不接收完整模块配置、密钥或日志脱敏上下文。
|
|
22
23
|
- `HAI_<配置名>_<路径>` 可整体覆盖根/对象/数组;父节点优先,未覆盖时才遍历已有叶子。camelCase 不拆词,约定变量优先于显式插值。
|
|
23
24
|
- 环境文件采用六类:`.env.example` 完整列出字段用途、默认值、枚举和范围;`.env` 为公共基础;`.env.dev`、`.env.test`、`.env.prod` 为公共环境覆盖,这五类均提交 Git。`.env.local` 为本机私有配置,不提交 Git。优先级为进程环境 > `.env.local` > 环境文件 > `.env`;Vite 在 Node 配置阶段调用 `core.config.loadEnvFiles({ mode, envDir })` 并检查 success 后注入进程,Node 脚本按相同顺序传入 `--env-file`。`core.init` 不自行读取 dotenv。
|
|
24
25
|
- 配置 getter 与 ID/logger 方法不是 HaiResult;validate/load 等须检查 success。
|
|
26
|
+
- 日志可用 `{ error }` 记录原生 Error;两端先规范化诊断再字段/URL 脱敏,配置 redact 路径仍生效。原异常不变,错误子图循环/深度用固定标记;message/stack 自由文本中的秘密需调用方先清理或遮蔽整条路径。
|
|
25
27
|
- 初始化前创建的默认、模块与嵌套 child Logger 在后续输出中应用当前全局 logging;configure/setLevel 也作用于已有实例。create 的显式 level/format/context 优先,未指定项动态继承。浏览器 json/pretty 与附加 redact 路径实际生效,消息文本仍由调用方脱敏。
|
|
26
28
|
|
|
27
29
|
## 按需参考
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
按任务读取 [reference.md](reference.md) 的对应小节;签名以当前源码或安装版本的公开类型为准,不默认加载全部资料。示例中的业务变量由应用提供。
|
|
30
32
|
|
|
31
33
|
- 运行环境
|
|
32
34
|
- 适用场景
|
|
@@ -36,3 +38,5 @@ description: "使用 @h-ai/core 配置、日志、i18n、ID、Zod 错误本地
|
|
|
36
38
|
- 常见模式
|
|
37
39
|
- 相关 Skills
|
|
38
40
|
- 浏览器侧使用
|
|
41
|
+
|
|
42
|
+
Node 请求/SSR 必须用 `core.i18n.runWithLocale(locale, callback)`(kit 同名方法)包裹实际异步 resolve;serv 自动建立请求语言上下文。浏览器 setGlobalLocale/createLocaleStore 继续管理应用语言,SSR 的 UI setter/store 不写全局值。显式 MessageOptions.locale 优先;进程默认语言仅在启动时设置。
|