@wdyy/skills 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "name": "wdyy-api-standard",
15
15
  "description": "根据“获取XX数据”“使用XX接口”“使用指定查询名称”等具体需求,定位已登记的接口 reference;对于 HIS SQL 查询,从远程查询目录按 name 精确匹配,并按已确认的接口契约将调用方式写入程序。Use when 需要查询或实现一个已登记接口或 HIS SQL 查询的数据调用时。",
16
- "files": ["SKILL.md", "agents/openai.yaml", "reference/api-demo.md", "reference/api-his-sql.md", "reference/api-sms.md"]
16
+ "files": ["SKILL.md", "agents/openai.yaml", "reference/api-demo.md", "reference/api-his-sql.md", "reference/api-sms.md", "reference/api-sso.md", "scripts/ensure-sso-env-example.mjs", "scripts/ensure-sso-env-example.test.mjs"]
17
17
  },
18
18
  {
19
19
  "name": "wdyy-bug-record",
@@ -20,9 +20,10 @@ disable-model-invocation: true
20
20
 
21
21
  1. 根据需求中的数据名称、业务对象或接口名称,在 `reference/api-*.md` 中查找候选业务接口,排除仅用于说明格式的 `api-demo.md`。
22
22
  2. 若需求为“使用 `<查询名称>`”且未直接匹配接口名称,则读取 `reference/api-his-sql.md`,按其中的远程目录和查询名称匹配规则处理。
23
- 3. 仅在唯一匹配时读取对应 reference,使用其中的接口 URL、JSON 入参和 JSON 返回值编写调用方式。
24
- 4. reference 的“备注”实现特殊调用要求。
25
- 5. 若没有匹配接口、存在多个候选接口,或 URL、入参、返回值不完整,先要求补充或确认接口信息;不得编造调用细节。
23
+ 3. 仅在唯一匹配时读取对应 reference。匹配 `api-sso` 时,在写入调用代码前运行 `node <Skill目录>/scripts/ensure-sso-env-example.mjs --target <目标项目根目录>`:目标 `.env.example` 缺失、`SSO_BASE_URL` 重复或已有不同值时,明确报告错误并等待工程师处理;工程师确认替换唯一的不同值后,才使用 `--replace-existing` 重试。不得创建或修改 `.env`。
24
+ 4. 使用对应 reference 的接口 URL、JSON 入参和 JSON 返回值编写调用方式。
25
+ 5. reference 的“备注”实现特殊调用要求。
26
+ 6. 若没有匹配接口、存在多个候选接口,或 URL、入参、返回值不完整,先要求补充或确认接口信息;不得编造调用细节。
26
27
 
27
28
  ## 接口文档约定
28
29
 
@@ -0,0 +1,87 @@
1
+ # 接口名称:api-sso
2
+
3
+ # 接口用途:接入统一认证中心,完成外部系统登录、Token 状态校验和登录态用户修改密码
4
+
5
+ # 接口URL:
6
+
7
+ 默认认证中心基础地址为 `http://172.26.1.80:9002`。使用本接口前,先运行 `node <Skill目录>/scripts/ensure-sso-env-example.mjs --target <目标项目根目录>`,把 `SSO_BASE_URL=http://172.26.1.80:9002` 写入目标项目已有的 `.env.example`。部署环境地址如有差异,以认证中心提供的实际地址为准;不得在代码中硬编码认证中心地址、端口或 JWT 签名密钥。接口路径如下:
8
+
9
+ | 用途 | 方法 | 路径 |
10
+ |---|---|---|
11
+ | 外部系统登录 | `POST` | `/api/v1/auth/login` |
12
+ | Token 验签 | `POST` | `/api/v1/auth/verify` |
13
+ | 修改密码 | `POST` | `/api/v1/auth/change-password` |
14
+
15
+ 三者均使用 `application/json; charset=utf-8`。登录无需 Token;验签和改密请求使用 `Authorization: Bearer <JWT_TOKEN>` 传入登录成功后取得的 Token。
16
+
17
+ # 入参格式:JSON
18
+
19
+ 登录请求:
20
+
21
+ ```json
22
+ {
23
+ "employee_id": "<EMPLOYEE_ID>",
24
+ "password": "<PASSWORD>",
25
+ "sys_id": "<SYSTEM_ID>"
26
+ }
27
+ ```
28
+
29
+ - `employee_id` 与 `username` 二选一,均按用户工号处理。
30
+ - `password`、`sys_id` 必填;`sys_id` 为已启用接入系统的 ID,可为 number 或 string。
31
+
32
+ 验签请求可使用空 JSON 请求体,并在请求头携带 Bearer Token。修改密码请求:
33
+
34
+ ```json
35
+ {
36
+ "old_password": "<OLD_PASSWORD>",
37
+ "new_password": "<NEW_PASSWORD>"
38
+ }
39
+ ```
40
+
41
+ - 改密仍须在请求头携带 Bearer Token。
42
+ - `new_password` 长度必须为 6 至 100 个字符。
43
+
44
+ # 返回值:JSON
45
+
46
+ 外部系统登录成功时,响应结构为:
47
+
48
+ ```json
49
+ {
50
+ "code": 200,
51
+ "message": "<DISPLAY_MESSAGE>",
52
+ "data": {
53
+ "allowed": true,
54
+ "user_id": "<USER_ID>",
55
+ "employee_id": "<EMPLOYEE_ID>",
56
+ "name": "<USER_NAME>",
57
+ "token": "<JWT_TOKEN>",
58
+ "permissions": [
59
+ {
60
+ "dept_id": "<DEPARTMENT_ID>",
61
+ "dept_name": "<DEPARTMENT_NAME>",
62
+ "roles": [
63
+ {
64
+ "role_id": "<ROLE_ID>",
65
+ "role_name": "<ROLE_NAME>",
66
+ "description": "<ROLE_DESCRIPTION>"
67
+ }
68
+ ]
69
+ }
70
+ ]
71
+ }
72
+ }
73
+ ```
74
+
75
+ 验签成功时 `data` 含 `valid`、`allowed`、`user_id`、`employee_id`、`name` 和 `sys_id`。改密成功时 `data` 含 `user_id`、`employee_id` 和 `name`。
76
+
77
+ 所有接口使用 `{ code, message, data }` 结构。HTTP 状态码及 `code` 的含义:`200` 成功、`400` 参数错误、`401` 认证失败或 Token 无效/过期、`403` 无目标系统权限或权限已失效、`404` 接入系统不存在或已停用、`500` 服务端异常。
78
+
79
+ # 备注:
80
+
81
+ - 不得依赖 `message` 的精确文案;登录和验签根据 HTTP 状态码、`code`、`data.valid`、`data.allowed` 判断结果。
82
+ - 登录成功后仅在需要的安全存储中保存 Token;日志、异常上报和前端状态中不得明文记录密码、Token、JWT 签名密钥或连接信息。
83
+ - 仅当登录响应 `data.allowed` 为 `true` 才进入目标系统;根据 `permissions` 内按科室聚合的角色控制业务入口。
84
+ - 服务端需要确认登录态或权限是否仍有效时调用验签接口。验签返回 `valid: false` 或 `allowed: false` 时,清除本地登录态并要求重新登录或联系管理员。
85
+ - 用户改密成功后,清除旧 Token 并提示用户使用新密码重新登录。
86
+ - 不得用管理端 `/api/auth/*` 接口替代本 reference 中的外部系统 `/api/v1/auth/*` 接口。
87
+ - `.env.example` 缺失时明确报错,不得创建该文件或修改 `.env`。已有相同 `SSO_BASE_URL` 时不重复写入;已有不同值时保留原值并等待工程师确认。工程师确认替换唯一的不同值后,才使用 `--replace-existing`;重复定义时始终报错。
@@ -0,0 +1,76 @@
1
+ import { access, readFile, writeFile } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ const DEFAULT_SSO_BASE_URL = 'http://172.26.1.80:9002';
6
+
7
+ function usage() {
8
+ throw new Error('Usage: node ensure-sso-env-example.mjs --target <project-root> [--replace-existing]');
9
+ }
10
+
11
+ function parseArguments(argumentsList) {
12
+ let target;
13
+ let replaceExisting = false;
14
+
15
+ for (let index = 0; index < argumentsList.length; index += 1) {
16
+ const argument = argumentsList[index];
17
+ if (argument === '--target') {
18
+ if (target || index + 1 >= argumentsList.length) usage();
19
+ target = argumentsList[index + 1];
20
+ index += 1;
21
+ } else if (argument === '--replace-existing') {
22
+ if (replaceExisting) usage();
23
+ replaceExisting = true;
24
+ } else {
25
+ usage();
26
+ }
27
+ }
28
+
29
+ if (!target) usage();
30
+ return { target: resolve(target), replaceExisting };
31
+ }
32
+
33
+ function findSsoBaseUrlLines(content) {
34
+ return content.split(/(?<=\n)/).flatMap((line, index) => {
35
+ const match = line.match(/^SSO_BASE_URL=([^\r\n]*)(\r?\n)?$/);
36
+ return match ? [{ index, value: match[1], line, newline: match[2] ?? '' }] : [];
37
+ });
38
+ }
39
+
40
+ async function main() {
41
+ const { target, replaceExisting } = parseArguments(process.argv.slice(2));
42
+ const examplePath = resolve(target, '.env.example');
43
+
44
+ try {
45
+ await access(examplePath, constants.F_OK);
46
+ } catch {
47
+ throw new Error(`Missing required configuration file: ${examplePath}`);
48
+ }
49
+
50
+ const content = await readFile(examplePath, 'utf8');
51
+ const entries = findSsoBaseUrlLines(content);
52
+ if (entries.length > 1) throw new Error('Duplicate SSO_BASE_URL definitions in .env.example');
53
+
54
+ if (entries.length === 0) {
55
+ const separator = content.length === 0 || content.endsWith('\n') ? '' : '\n';
56
+ await writeFile(examplePath, `${content}${separator}SSO_BASE_URL=${DEFAULT_SSO_BASE_URL}\n`);
57
+ process.stdout.write(`Added SSO_BASE_URL to ${examplePath}\n`);
58
+ return;
59
+ }
60
+
61
+ const [entry] = entries;
62
+ if (entry.value === DEFAULT_SSO_BASE_URL) {
63
+ process.stdout.write(`SSO_BASE_URL is already configured in ${examplePath}\n`);
64
+ return;
65
+ }
66
+
67
+ if (!replaceExisting) {
68
+ throw new Error('SSO_BASE_URL already has a different value; retain it and obtain engineer confirmation before using --replace-existing');
69
+ }
70
+
71
+ const replacement = `SSO_BASE_URL=${DEFAULT_SSO_BASE_URL}${entry.newline}`;
72
+ await writeFile(examplePath, content.replace(entry.line, replacement));
73
+ process.stdout.write(`Replaced SSO_BASE_URL in ${examplePath}\n`);
74
+ }
75
+
76
+ await main();
@@ -0,0 +1,66 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFile } from 'node:child_process';
3
+ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import test from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promisify } from 'node:util';
9
+
10
+ const run = promisify(execFile);
11
+ const script = join(dirname(fileURLToPath(import.meta.url)), 'ensure-sso-env-example.mjs');
12
+ const defaultValue = 'SSO_BASE_URL=http://172.26.1.80:9002';
13
+ const created = [];
14
+
15
+ async function fixture(content) {
16
+ const root = await mkdtemp(join(tmpdir(), 'wdyy-api-sso-'));
17
+ created.push(root);
18
+ if (content !== undefined) await writeFile(join(root, '.env.example'), content);
19
+ return root;
20
+ }
21
+
22
+ async function invoke(root, ...argumentsList) {
23
+ return run(process.execPath, [script, '--target', root, ...argumentsList]);
24
+ }
25
+
26
+ test.after(async () => {
27
+ await Promise.all(created.map((path) => rm(path, { recursive: true, force: true })));
28
+ });
29
+
30
+ test('缺少 .env.example 时明确失败且不创建文件', async () => {
31
+ const root = await fixture();
32
+ await assert.rejects(invoke(root), /Missing required configuration file/);
33
+ await assert.rejects(readFile(join(root, '.env.example')));
34
+ });
35
+
36
+ test('追加默认 SSO 地址且保持 .env.example 其他内容', async () => {
37
+ const root = await fixture('API_URL=/api\n');
38
+ await invoke(root);
39
+ assert.equal(await readFile(join(root, '.env.example'), 'utf8'), `API_URL=/api\n${defaultValue}\n`);
40
+ });
41
+
42
+ test('已存在默认 SSO 地址时不重复写入', async () => {
43
+ const root = await fixture(`${defaultValue}\n`);
44
+ await invoke(root);
45
+ assert.equal(await readFile(join(root, '.env.example'), 'utf8'), `${defaultValue}\n`);
46
+ });
47
+
48
+ test('不同 SSO 地址默认保留且明确失败', async () => {
49
+ const root = await fixture('SSO_BASE_URL=http://sso.internal:9002\n');
50
+ const before = await readFile(join(root, '.env.example'));
51
+ await assert.rejects(invoke(root), /different value/);
52
+ assert.deepEqual(await readFile(join(root, '.env.example')), before);
53
+ });
54
+
55
+ test('重复 SSO 地址明确失败且不修改文件', async () => {
56
+ const root = await fixture(`${defaultValue}\nSSO_BASE_URL=http://sso.internal:9002\n`);
57
+ const before = await readFile(join(root, '.env.example'));
58
+ await assert.rejects(invoke(root), /Duplicate SSO_BASE_URL definitions/);
59
+ assert.deepEqual(await readFile(join(root, '.env.example')), before);
60
+ });
61
+
62
+ test('明确替换后更新唯一的不同 SSO 地址', async () => {
63
+ const root = await fixture('SSO_BASE_URL=http://sso.internal:9002\n');
64
+ await invoke(root, '--replace-existing');
65
+ assert.equal(await readFile(join(root, '.env.example'), 'utf8'), `${defaultValue}\n`);
66
+ });
package/lib/wdyy-cli.js CHANGED
@@ -43,19 +43,19 @@ BACKEND_URL=0.0.0.0
43
43
  BACKEND_PORT=3000
44
44
 
45
45
  # 数据库
46
- DB_URL=
46
+ DB_URL=localhost
47
47
  DB_PORT=5432
48
- DB_USER=
48
+ DB_USER=lhy
49
49
  DB_PASSWORD=
50
- DB_NAME=
51
- DB_SCHEMA=
50
+ DB_NAME=hospital
51
+ DB_SCHEMA=api_platform
52
52
 
53
53
  # API
54
54
 
55
55
  # 部署
56
56
  `;
57
57
 
58
- const defaultProjectLayout = [...defaultProjectDirectories, '.env', '.env.example'];
58
+ const defaultProjectLayout = [...defaultProjectDirectories, '.env.example'];
59
59
 
60
60
  const generatedAgentsContent = `# 企业开发规则
61
61
 
@@ -72,6 +72,8 @@ const generatedAgentsContent = `# 企业开发规则
72
72
 
73
73
  所有 URL、端口和 IP 地址必须从项目根目录的 \`.env\` 文件读取,禁止在代码中硬编码;不得提交秘密或生产环境 \`.env\`。
74
74
 
75
+ \`.env.example\` 是配置模板。需要本地配置时,必须先手工复制 \`.env.example\` 为 \`.env\`;后续修改配置文件时,必须先修改 \`.env.example\`,再同步修改 \`.env\`。不得将真实密码、密钥或生产配置写入 \`.env.example\`。
76
+
75
77
  项目开发完成后必须创建 \`README.md\`,内容包括项目技术栈、目录结构和安装部署说明。
76
78
 
77
79
  ## Skill 路由
@@ -181,18 +183,16 @@ async function createDefaultProjectLayout(targetRoot, writtenPaths) {
181
183
  if (!existed) writtenPaths.push(destination);
182
184
  }
183
185
 
184
- for (const environmentFile of ['.env', '.env.example']) {
185
- const environmentPath = join(targetRoot, environmentFile);
186
- try {
187
- await access(environmentPath);
188
- } catch (error) {
189
- if (error.code !== 'ENOENT') throw error;
190
- await writeFile(environmentPath, defaultEnvironmentContent, {
191
- encoding: 'utf8',
192
- mode: environmentFile === '.env' ? 0o600 : 0o644
193
- });
194
- writtenPaths.push(environmentPath);
195
- }
186
+ const environmentExamplePath = join(targetRoot, '.env.example');
187
+ try {
188
+ await access(environmentExamplePath);
189
+ } catch (error) {
190
+ if (error.code !== 'ENOENT') throw error;
191
+ await writeFile(environmentExamplePath, defaultEnvironmentContent, {
192
+ encoding: 'utf8',
193
+ mode: 0o644
194
+ });
195
+ writtenPaths.push(environmentExamplePath);
196
196
  }
197
197
  console.log(`已准备默认项目目录: ${defaultProjectDirectories.join('、')}`);
198
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdyy/skills",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "企业内部开发 Skill 与项目初始化命令",
5
5
  "type": "module",
6
6
  "bin": {