Files

1 line
35 KiB
JSON

{"repo": "charliehzm/medharness", "files": {"scripts/seed_medical_channels.py": {"4c3a134e216bb51ead81b9efb47e394d1f96028b": {"t": 1785747436.1238456, "n": 6717, "h": "60715e164430883a", "c": "#!/usr/bin/env python3\n\"\"\"Seed the medical channel TEMPLATES into new-api as DISABLED, keyless channels.\n\nReads deploy/presets/channel-templates.medical.json and creates one new-api channel per\ntemplate — key left BLANK and status=disabled — so NOTHING is callable until an operator\nfills in the real key / base_url and meets the compliance prerequisites (no-retention /\n境内 driving contract / private deployment). Idempotent: skips a channel whose name\nalready exists. This script NEVER writes a real key or a live (enabled) channel; it\nrefuses any template that carries a non-empty key.\n\nRun inside the A0 container, which can reach new-api:\n\n docker cp scripts/seed_medical_channels.py medharness-a0-api:/tmp/\n docker cp deploy/presets/channel-templates.medical.json medharness-a0-api:/tmp/\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json --dry-run # preview\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json # create (disabled)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport http.cookiejar\nimport json\nimport os\nimport sys\nimport urllib.request\nfrom pathlib import Path\n\nNEW_API = os.environ.get(\"NEW_API_URL\", \"http://new-api:3000\").rstrip(\"/\")\nUSER = os.environ.get(\"NEW_API_ROOT_USERNAME\", \"admin\")\nPW = os.environ.get(\"NEW_API_ROOT_PASSWORD\", \"medharness123\")\n\nDEFAULT_TEMPLATES = (\n Path(__file__).resolve().parents[1] / \"deploy\" / \"presets\" / \"channel-templates.medical.json\"\n)\n\n# new-api channel type ints (constant/channel.go); covers the provider names the medical\n# templates use. Unknown -> refuse (a wrong int would silently mis-route).\nTYPE_NAME_TO_INT = {\n \"openai\": 1,\n \"azure\": 3,\n \"ollama\": 4,\n \"custom\": 8,\n \"openai-compatible\": 8,\n \"anthropic\": 14,\n \"claude\": 14,\n \"baidu\": 15,\n \"baichuan\": 15,\n \"zhipu\": 16,\n \"ali\": 17,\n \"qwen\": 17,\n \"dashscope\": 17,\n \"gemini\": 24,\n \"google\": 24,\n \"moonshot\": 25,\n \"deepseek\": 43,\n \"siliconflow\": 40,\n \"vllm\": 47,\n \"xinference\": 47,\n}\nSTATUS_NAME_TO_INT = {\"enabled\": 1, \"disabled\": 2}\n\n# new-api's validateChannel refuses an empty key on add, so a seeded template gets an\n# obviously-fake placeholder key (the operator MUST replace it). The channel is created\n# disabled, so the placeholder is never used to call anything.\nPLACEHOLDER_KEY = \"REPLACE_WITH_REAL_KEY_BEFORE_ENABLING\"\n\n\ndef _call(opener, path, body=None, method=None):\n data = json.dumps(body).encode() if body is not None else None\n req = urllib.request.Request(\n NEW_API + path, data=data, method=method or (\"POST\" if body is not None else \"GET\")\n )\n req.add_header(\"Content-Type\", \"application/json\")\n req.add_header(\"New-Api-User\", \"1\")\n with opener.open(req, timeout=15) as resp:\n raw = resp.read().decode()\n return json.loads(raw) if raw.strip() else {}\n\n\ndef _existing_names(opener) -> set[str]:\n data = _call(opener, \"/api/channel/?p=1&size=200\").get(\"data\")\n items = data.get(\"items\") if isinstance(data, dict) else data\n return {c.get(\"name\") for c in (items or []) if isinstance(c, dict)}\n\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n description=\"Seed disabled, keyless medical channel templates into new-api.\"\n )\n ap.add_argument(\n \"--templates\",\n type=Path,\n default=DEFAULT_TEMPLATES,\n help=\"path to channel-templates.medical.json\",\n )\n ap.add_argument(\"--dry-run\", action=\"store_true\", help=\"print the plan, create nothing\")\n args = ap.parse_args()\n\n doc = json.loads(args.templates.read_text(encoding=\"utf-8\"))\n channels = doc.get(\"channels\", [])\n if not channels:\n print(\"no channel templates found\", file=sys.stderr)\n return 1\n\n # Honesty guard: refuse to seed anything that is not a blank-key, disabled template.\n for ch in channels:\n if ch.get(\"key\"):\n print(f\"REFUSING: template {ch.get('name')!r} carries a non-empty key\", file=sys.stderr)\n return 2\n if ch.get(\"status\") != \"disabled\":\n print(f\"REFUSING: template {ch.get('name')!r} is not status=disabled\", file=sys.stderr)\n return 2\n if str(ch.get(\"type\", \"\")).lower() not in TYPE_NAME_TO_INT:\n print(\n f\"REFUSING: template {ch.get('name')!r} has unknown type {ch.get('type')!r}\",\n file=sys.stderr,\n )\n return 2\n\n if args.dry_run:\n print(f\"[dry-run] would seed {len(channels)} DISABLED, keyless channels into {NEW_API}:\")\n for ch in channels:\n mh = ch.get(\"medharness\", {})\n print(\n f\" · {ch['name']} type={ch['type']}->{TYPE_NAME_TO_INT[ch['type'].lower()]} \"\n f\"models={','.join(ch.get('models', []))} group={ch.get('group')} \"\n f\"lane={mh.get('lane')} region={mh.get('region')} cap={mh.get('data_level_cap')} [placeholder key · disabled]\"\n )\n return 0\n\n opener = urllib.request.build_opener(\n urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())\n )\n login = _call(opener, \"/api/user/login\", {\"username\": USER, \"password\": PW})\n if not login.get(\"success\"):\n print(\"new-api admin login failed\", file=sys.stderr)\n return 1\n\n existing = _existing_names(opener)\n created = skipped = 0\n for ch in channels:\n if ch[\"name\"] in existing:\n print(f\" skip (exists): {ch['name']}\")\n skipped += 1\n continue\n wrapped = {\n \"mode\": \"single\",\n \"channel\": {\n \"name\": ch[\"name\"],\n \"type\": TYPE_NAME_TO_INT[ch[\"type\"].lower()],\n \"key\": PLACEHOLDER_KEY, # obvious placeholder — operator MUST replace it\n \"base_url\": ch.get(\"base_url\", \"\"),\n \"models\": \",\".join(ch.get(\"models\", [])),\n \"group\": ch.get(\"group\", \"default\"),\n \"status\": STATUS_NAME_TO_INT[\"disabled\"],\n },\n }\n res = _call(opener, \"/api/channel/\", wrapped)\n if res.get(\"success\"):\n print(f\" created (disabled): {ch['name']}\")\n created += 1\n else:\n print(f\" FAILED: {ch['name']}: {res.get('message')}\", file=sys.stderr)\n print(\n f\"done: {created} created, {skipped} skipped (already present). All DISABLED with a \"\n f\"placeholder key ({PLACEHOLDER_KEY!r}) — replace key + base_url and meet the compliance \"\n f\"prerequisites before enabling.\"\n )\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"}, "62bd75177aabbd6722ad9c5b15dac50826d31b65": {"t": 1785773297.7909842, "n": 6717, "h": "60715e164430883a", "c": "#!/usr/bin/env python3\n\"\"\"Seed the medical channel TEMPLATES into new-api as DISABLED, keyless channels.\n\nReads deploy/presets/channel-templates.medical.json and creates one new-api channel per\ntemplate — key left BLANK and status=disabled — so NOTHING is callable until an operator\nfills in the real key / base_url and meets the compliance prerequisites (no-retention /\n境内 driving contract / private deployment). Idempotent: skips a channel whose name\nalready exists. This script NEVER writes a real key or a live (enabled) channel; it\nrefuses any template that carries a non-empty key.\n\nRun inside the A0 container, which can reach new-api:\n\n docker cp scripts/seed_medical_channels.py medharness-a0-api:/tmp/\n docker cp deploy/presets/channel-templates.medical.json medharness-a0-api:/tmp/\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json --dry-run # preview\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json # create (disabled)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport http.cookiejar\nimport json\nimport os\nimport sys\nimport urllib.request\nfrom pathlib import Path\n\nNEW_API = os.environ.get(\"NEW_API_URL\", \"http://new-api:3000\").rstrip(\"/\")\nUSER = os.environ.get(\"NEW_API_ROOT_USERNAME\", \"admin\")\nPW = os.environ.get(\"NEW_API_ROOT_PASSWORD\", \"medharness123\")\n\nDEFAULT_TEMPLATES = (\n Path(__file__).resolve().parents[1] / \"deploy\" / \"presets\" / \"channel-templates.medical.json\"\n)\n\n# new-api channel type ints (constant/channel.go); covers the provider names the medical\n# templates use. Unknown -> refuse (a wrong int would silently mis-route).\nTYPE_NAME_TO_INT = {\n \"openai\": 1,\n \"azure\": 3,\n \"ollama\": 4,\n \"custom\": 8,\n \"openai-compatible\": 8,\n \"anthropic\": 14,\n \"claude\": 14,\n \"baidu\": 15,\n \"baichuan\": 15,\n \"zhipu\": 16,\n \"ali\": 17,\n \"qwen\": 17,\n \"dashscope\": 17,\n \"gemini\": 24,\n \"google\": 24,\n \"moonshot\": 25,\n \"deepseek\": 43,\n \"siliconflow\": 40,\n \"vllm\": 47,\n \"xinference\": 47,\n}\nSTATUS_NAME_TO_INT = {\"enabled\": 1, \"disabled\": 2}\n\n# new-api's validateChannel refuses an empty key on add, so a seeded template gets an\n# obviously-fake placeholder key (the operator MUST replace it). The channel is created\n# disabled, so the placeholder is never used to call anything.\nPLACEHOLDER_KEY = \"REPLACE_WITH_REAL_KEY_BEFORE_ENABLING\"\n\n\ndef _call(opener, path, body=None, method=None):\n data = json.dumps(body).encode() if body is not None else None\n req = urllib.request.Request(\n NEW_API + path, data=data, method=method or (\"POST\" if body is not None else \"GET\")\n )\n req.add_header(\"Content-Type\", \"application/json\")\n req.add_header(\"New-Api-User\", \"1\")\n with opener.open(req, timeout=15) as resp:\n raw = resp.read().decode()\n return json.loads(raw) if raw.strip() else {}\n\n\ndef _existing_names(opener) -> set[str]:\n data = _call(opener, \"/api/channel/?p=1&size=200\").get(\"data\")\n items = data.get(\"items\") if isinstance(data, dict) else data\n return {c.get(\"name\") for c in (items or []) if isinstance(c, dict)}\n\n\ndef main() -> int:\n ap = argparse.ArgumentParser(\n description=\"Seed disabled, keyless medical channel templates into new-api.\"\n )\n ap.add_argument(\n \"--templates\",\n type=Path,\n default=DEFAULT_TEMPLATES,\n help=\"path to channel-templates.medical.json\",\n )\n ap.add_argument(\"--dry-run\", action=\"store_true\", help=\"print the plan, create nothing\")\n args = ap.parse_args()\n\n doc = json.loads(args.templates.read_text(encoding=\"utf-8\"))\n channels = doc.get(\"channels\", [])\n if not channels:\n print(\"no channel templates found\", file=sys.stderr)\n return 1\n\n # Honesty guard: refuse to seed anything that is not a blank-key, disabled template.\n for ch in channels:\n if ch.get(\"key\"):\n print(f\"REFUSING: template {ch.get('name')!r} carries a non-empty key\", file=sys.stderr)\n return 2\n if ch.get(\"status\") != \"disabled\":\n print(f\"REFUSING: template {ch.get('name')!r} is not status=disabled\", file=sys.stderr)\n return 2\n if str(ch.get(\"type\", \"\")).lower() not in TYPE_NAME_TO_INT:\n print(\n f\"REFUSING: template {ch.get('name')!r} has unknown type {ch.get('type')!r}\",\n file=sys.stderr,\n )\n return 2\n\n if args.dry_run:\n print(f\"[dry-run] would seed {len(channels)} DISABLED, keyless channels into {NEW_API}:\")\n for ch in channels:\n mh = ch.get(\"medharness\", {})\n print(\n f\" · {ch['name']} type={ch['type']}->{TYPE_NAME_TO_INT[ch['type'].lower()]} \"\n f\"models={','.join(ch.get('models', []))} group={ch.get('group')} \"\n f\"lane={mh.get('lane')} region={mh.get('region')} cap={mh.get('data_level_cap')} [placeholder key · disabled]\"\n )\n return 0\n\n opener = urllib.request.build_opener(\n urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())\n )\n login = _call(opener, \"/api/user/login\", {\"username\": USER, \"password\": PW})\n if not login.get(\"success\"):\n print(\"new-api admin login failed\", file=sys.stderr)\n return 1\n\n existing = _existing_names(opener)\n created = skipped = 0\n for ch in channels:\n if ch[\"name\"] in existing:\n print(f\" skip (exists): {ch['name']}\")\n skipped += 1\n continue\n wrapped = {\n \"mode\": \"single\",\n \"channel\": {\n \"name\": ch[\"name\"],\n \"type\": TYPE_NAME_TO_INT[ch[\"type\"].lower()],\n \"key\": PLACEHOLDER_KEY, # obvious placeholder — operator MUST replace it\n \"base_url\": ch.get(\"base_url\", \"\"),\n \"models\": \",\".join(ch.get(\"models\", [])),\n \"group\": ch.get(\"group\", \"default\"),\n \"status\": STATUS_NAME_TO_INT[\"disabled\"],\n },\n }\n res = _call(opener, \"/api/channel/\", wrapped)\n if res.get(\"success\"):\n print(f\" created (disabled): {ch['name']}\")\n created += 1\n else:\n print(f\" FAILED: {ch['name']}: {res.get('message')}\", file=sys.stderr)\n print(\n f\"done: {created} created, {skipped} skipped (already present). All DISABLED with a \"\n f\"placeholder key ({PLACEHOLDER_KEY!r}) — replace key + base_url and meet the compliance \"\n f\"prerequisites before enabling.\"\n )\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"}, "e54f458ac2b3bab8465b9cc91a548c25d410973f": {"t": 1785773299.1137207, "n": 6414, "h": "47203640e0074caa", "c": "#!/usr/bin/env python3\n\"\"\"Seed the medical channel TEMPLATES into new-api as DISABLED, keyless channels.\n\nReads deploy/presets/channel-templates.medical.json and creates one new-api channel per\ntemplate — key left BLANK and status=disabled — so NOTHING is callable until an operator\nfills in the real key / base_url and meets the compliance prerequisites (no-retention /\n境内 driving contract / private deployment). Idempotent: skips a channel whose name\nalready exists. This script NEVER writes a real key or a live (enabled) channel; it\nrefuses any template that carries a non-empty key.\n\nRun inside the A0 container, which can reach new-api:\n\n docker cp scripts/seed_medical_channels.py medharness-a0-api:/tmp/\n docker cp deploy/presets/channel-templates.medical.json medharness-a0-api:/tmp/\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json --dry-run # preview\n docker exec medharness-a0-api python /tmp/seed_medical_channels.py \\\n --templates /tmp/channel-templates.medical.json # create (disabled)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport http.cookiejar\nimport json\nimport os\nimport sys\nimport urllib.request\nfrom pathlib import Path\n\nNEW_API = os.environ.get(\"NEW_API_URL\", \"http://new-api:3000\").rstrip(\"/\")\nUSER = os.environ.get(\"NEW_API_ROOT_USERNAME\", \"admin\")\nPW = os.environ.get(\"NEW_API_ROOT_PASSWORD\", \"medharness123\")\n\nDEFAULT_TEMPLATES = Path(__file__).resolve().parents[1] / \"deploy\" / \"presets\" / \"channel-templates.medical.json\"\n\n# new-api channel type ints (constant/channel.go); covers the provider names the medical\n# templates use. Unknown -> refuse (a wrong int would silently mis-route).\nTYPE_NAME_TO_INT = {\n \"openai\": 1, \"azure\": 3, \"ollama\": 4, \"custom\": 8, \"openai-compatible\": 8,\n \"anthropic\": 14, \"claude\": 14, \"baidu\": 15, \"baichuan\": 15, \"zhipu\": 16,\n \"ali\": 17, \"qwen\": 17, \"dashscope\": 17, \"gemini\": 24, \"google\": 24,\n \"moonshot\": 25, \"deepseek\": 43, \"siliconflow\": 40, \"vllm\": 47, \"xinference\": 47,\n}\nSTATUS_NAME_TO_INT = {\"enabled\": 1, \"disabled\": 2}\n\n# new-api's validateChannel refuses an empty key on add, so a seeded template gets an\n# obviously-fake placeholder key (the operator MUST replace it). The channel is created\n# disabled, so the placeholder is never used to call anything.\nPLACEHOLDER_KEY = \"REPLACE_WITH_REAL_KEY_BEFORE_ENABLING\"\n\n\ndef _call(opener, path, body=None, method=None):\n data = json.dumps(body).encode() if body is not None else None\n req = urllib.request.Request(NEW_API + path, data=data, method=method or (\"POST\" if body is not None else \"GET\"))\n req.add_header(\"Content-Type\", \"application/json\")\n req.add_header(\"New-Api-User\", \"1\")\n with opener.open(req, timeout=15) as resp:\n raw = resp.read().decode()\n return json.loads(raw) if raw.strip() else {}\n\n\ndef _existing_names(opener) -> set[str]:\n data = _call(opener, \"/api/channel/?p=1&size=200\").get(\"data\")\n items = data.get(\"items\") if isinstance(data, dict) else data\n return {c.get(\"name\") for c in (items or []) if isinstance(c, dict)}\n\n\ndef main() -> int:\n ap = argparse.ArgumentParser(description=\"Seed disabled, keyless medical channel templates into new-api.\")\n ap.add_argument(\"--templates\", type=Path, default=DEFAULT_TEMPLATES, help=\"path to channel-templates.medical.json\")\n ap.add_argument(\"--dry-run\", action=\"store_true\", help=\"print the plan, create nothing\")\n args = ap.parse_args()\n\n doc = json.loads(args.templates.read_text(encoding=\"utf-8\"))\n channels = doc.get(\"channels\", [])\n if not channels:\n print(\"no channel templates found\", file=sys.stderr)\n return 1\n\n # Honesty guard: refuse to seed anything that is not a blank-key, disabled template.\n for ch in channels:\n if ch.get(\"key\"):\n print(f\"REFUSING: template {ch.get('name')!r} carries a non-empty key\", file=sys.stderr)\n return 2\n if ch.get(\"status\") != \"disabled\":\n print(f\"REFUSING: template {ch.get('name')!r} is not status=disabled\", file=sys.stderr)\n return 2\n if str(ch.get(\"type\", \"\")).lower() not in TYPE_NAME_TO_INT:\n print(f\"REFUSING: template {ch.get('name')!r} has unknown type {ch.get('type')!r}\", file=sys.stderr)\n return 2\n\n if args.dry_run:\n print(f\"[dry-run] would seed {len(channels)} DISABLED, keyless channels into {NEW_API}:\")\n for ch in channels:\n mh = ch.get(\"medharness\", {})\n print(f\" · {ch['name']} type={ch['type']}->{TYPE_NAME_TO_INT[ch['type'].lower()]} \"\n f\"models={','.join(ch.get('models', []))} group={ch.get('group')} \"\n f\"lane={mh.get('lane')} region={mh.get('region')} cap={mh.get('data_level_cap')} [placeholder key · disabled]\")\n return 0\n\n opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))\n login = _call(opener, \"/api/user/login\", {\"username\": USER, \"password\": PW})\n if not login.get(\"success\"):\n print(\"new-api admin login failed\", file=sys.stderr)\n return 1\n\n existing = _existing_names(opener)\n created = skipped = 0\n for ch in channels:\n if ch[\"name\"] in existing:\n print(f\" skip (exists): {ch['name']}\")\n skipped += 1\n continue\n wrapped = {\"mode\": \"single\", \"channel\": {\n \"name\": ch[\"name\"],\n \"type\": TYPE_NAME_TO_INT[ch[\"type\"].lower()],\n \"key\": PLACEHOLDER_KEY, # obvious placeholder — operator MUST replace it\n \"base_url\": ch.get(\"base_url\", \"\"),\n \"models\": \",\".join(ch.get(\"models\", [])),\n \"group\": ch.get(\"group\", \"default\"),\n \"status\": STATUS_NAME_TO_INT[\"disabled\"],\n }}\n res = _call(opener, \"/api/channel/\", wrapped)\n if res.get(\"success\"):\n print(f\" created (disabled): {ch['name']}\")\n created += 1\n else:\n print(f\" FAILED: {ch['name']}: {res.get('message')}\", file=sys.stderr)\n print(f\"done: {created} created, {skipped} skipped (already present). All DISABLED with a \"\n f\"placeholder key ({PLACEHOLDER_KEY!r}) — replace key + base_url and meet the compliance \"\n f\"prerequisites before enabling.\")\n return 0\n\n\nif __name__ == \"__main__\":\n raise SystemExit(main())\n"}}, "web/e2e/access-channels-mgmt.spec.ts": {"4c3a134e216bb51ead81b9efb47e394d1f96028b": {"t": 1785747438.318578, "n": 4311, "h": "5b931e3412896ded", "c": "import { expect, test, type Page } from \"@playwright/test\";\n\nimport { assertNoPatientPhiDom, gotoScreen, login } from \"./fixtures\";\n\nasync function assertNoSecretsOrPatientPhi(page: Page, where: string): Promise<void> {\n const html = await page.content();\n expect(html, `plaintext key marker on ${where}`).not.toContain(\"sk-\");\n await assertNoPatientPhiDom(page, where);\n}\n\ntest(\"sysadmin manages access apps and model channels without exposing secrets\", async ({ page }) => {\n await login(page);\n await expect(page.locator(\".role-select button.on\")).toContainText(\"系统管理员\");\n await gotoScreen(page, \"接入\");\n\n const suffix = Math.random().toString(36).slice(2, 7);\n const appName = `自动化接入应用${suffix}`;\n const channelName = `自动化模型渠道${suffix}`;\n\n await page.getByRole(\"button\", { name: \"新建接入应用\" }).click();\n await page.getByLabel(\"应用名\").fill(appName);\n await page.getByLabel(\"剩余配额\").fill(\"180000\");\n // Data levels are read-only (derived from the group), so there is nothing to tick here.\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·新建\");\n\n // The token row shows its derived data levels read-only (no 数据等级 edit action).\n const appRow = page.locator(\"tr\", { hasText: appName });\n await expect(appRow).toContainText(\"L2\");\n await expect(appRow.getByRole(\"button\", { name: \"数据等级\" })).toHaveCount(0);\n\n await appRow.getByRole(\"button\", { name: \"改配额\" }).click();\n await page.getByLabel(\"剩余配额\").fill(\"220000\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"220000\");\n await assertNoSecretsOrPatientPhi(page, \"接入应用·改配额\");\n\n await appRow.getByRole(\"button\", { name: \"停用\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"停用\");\n\n await appRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·删除\");\n\n await page.locator('.access-tabs button:has-text(\"模型与渠道\")').click();\n await page.getByRole(\"button\", { name: \"新建渠道\" }).click();\n // 从医疗模型目录预填: picking a template fills name/type/models for the operator.\n await page.getByLabel(\"从医疗模型目录预填\").selectOption({ label: \"DeepSeek 境内·常规便宜池 (L2)\" });\n await expect(page.getByLabel(\"渠道名称\")).toHaveValue(\"DeepSeek 境内·常规便宜池\");\n await expect(page.getByLabel(\"模型\", { exact: true })).toHaveValue(\"deepseek-v3\");\n // then override with the synthetic test channel for the rest of the CRUD flow.\n await page.getByLabel(\"渠道名称\").fill(channelName);\n await page.getByLabel(\"类型\").fill(\"openai\");\n await page.getByLabel(\"权重\").fill(\"60\");\n await page.getByLabel(\"模型\", { exact: true }).fill(`model-${suffix}`);\n await page.getByLabel(\"服务地址\").fill(\"https://gateway.example.test/v1\");\n await page.getByLabel(\"密钥\").fill(`sk-test-${suffix}`);\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·新建\");\n\n const channelRow = page.locator(\"tr\", { hasText: channelName });\n await channelRow.getByRole(\"button\", { name: \"编辑\" }).click();\n await page.getByLabel(\"权重\").fill(\"55\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(channelRow).toContainText(\"55%\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·编辑\");\n\n await channelRow.getByRole(\"button\", { name: \"测试渠道\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n // The probe runs against an unreachable placeholder upstream → honest \"不可达\" verdict\n // (the operation succeeds; the modal stays open showing the result, then closes on 关闭).\n await expect(page.locator(\".access-test-result\")).toContainText(\"不可达\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·测试\");\n await page.getByRole(\"button\", { name: \"关闭\" }).click();\n await expect(page.locator(\".access-modal\")).toHaveCount(0);\n\n await channelRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·删除\");\n});\n"}, "9443a42ea693dd87b6929969f96718554f8deb37": {"t": 1785773302.9616373, "n": 4311, "h": "5b931e3412896ded", "c": "import { expect, test, type Page } from \"@playwright/test\";\n\nimport { assertNoPatientPhiDom, gotoScreen, login } from \"./fixtures\";\n\nasync function assertNoSecretsOrPatientPhi(page: Page, where: string): Promise<void> {\n const html = await page.content();\n expect(html, `plaintext key marker on ${where}`).not.toContain(\"sk-\");\n await assertNoPatientPhiDom(page, where);\n}\n\ntest(\"sysadmin manages access apps and model channels without exposing secrets\", async ({ page }) => {\n await login(page);\n await expect(page.locator(\".role-select button.on\")).toContainText(\"系统管理员\");\n await gotoScreen(page, \"接入\");\n\n const suffix = Math.random().toString(36).slice(2, 7);\n const appName = `自动化接入应用${suffix}`;\n const channelName = `自动化模型渠道${suffix}`;\n\n await page.getByRole(\"button\", { name: \"新建接入应用\" }).click();\n await page.getByLabel(\"应用名\").fill(appName);\n await page.getByLabel(\"剩余配额\").fill(\"180000\");\n // Data levels are read-only (derived from the group), so there is nothing to tick here.\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·新建\");\n\n // The token row shows its derived data levels read-only (no 数据等级 edit action).\n const appRow = page.locator(\"tr\", { hasText: appName });\n await expect(appRow).toContainText(\"L2\");\n await expect(appRow.getByRole(\"button\", { name: \"数据等级\" })).toHaveCount(0);\n\n await appRow.getByRole(\"button\", { name: \"改配额\" }).click();\n await page.getByLabel(\"剩余配额\").fill(\"220000\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"220000\");\n await assertNoSecretsOrPatientPhi(page, \"接入应用·改配额\");\n\n await appRow.getByRole(\"button\", { name: \"停用\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"停用\");\n\n await appRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·删除\");\n\n await page.locator('.access-tabs button:has-text(\"模型与渠道\")').click();\n await page.getByRole(\"button\", { name: \"新建渠道\" }).click();\n // 从医疗模型目录预填: picking a template fills name/type/models for the operator.\n await page.getByLabel(\"从医疗模型目录预填\").selectOption({ label: \"DeepSeek 境内·常规便宜池 (L2)\" });\n await expect(page.getByLabel(\"渠道名称\")).toHaveValue(\"DeepSeek 境内·常规便宜池\");\n await expect(page.getByLabel(\"模型\", { exact: true })).toHaveValue(\"deepseek-v3\");\n // then override with the synthetic test channel for the rest of the CRUD flow.\n await page.getByLabel(\"渠道名称\").fill(channelName);\n await page.getByLabel(\"类型\").fill(\"openai\");\n await page.getByLabel(\"权重\").fill(\"60\");\n await page.getByLabel(\"模型\", { exact: true }).fill(`model-${suffix}`);\n await page.getByLabel(\"服务地址\").fill(\"https://gateway.example.test/v1\");\n await page.getByLabel(\"密钥\").fill(`sk-test-${suffix}`);\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·新建\");\n\n const channelRow = page.locator(\"tr\", { hasText: channelName });\n await channelRow.getByRole(\"button\", { name: \"编辑\" }).click();\n await page.getByLabel(\"权重\").fill(\"55\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(channelRow).toContainText(\"55%\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·编辑\");\n\n await channelRow.getByRole(\"button\", { name: \"测试渠道\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n // The probe runs against an unreachable placeholder upstream → honest \"不可达\" verdict\n // (the operation succeeds; the modal stays open showing the result, then closes on 关闭).\n await expect(page.locator(\".access-test-result\")).toContainText(\"不可达\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·测试\");\n await page.getByRole(\"button\", { name: \"关闭\" }).click();\n await expect(page.locator(\".access-modal\")).toHaveCount(0);\n\n await channelRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·删除\");\n});\n"}, "abfc2e7c1feb9e231561546ab7cea42486011238": {"t": 1785773304.5890071, "n": 3898, "h": "38dd350d29842ede", "c": "import { expect, test, type Page } from \"@playwright/test\";\n\nimport { assertNoPatientPhiDom, gotoScreen, login } from \"./fixtures\";\n\nasync function assertNoSecretsOrPatientPhi(page: Page, where: string): Promise<void> {\n const html = await page.content();\n expect(html, `plaintext key marker on ${where}`).not.toContain(\"sk-\");\n await assertNoPatientPhiDom(page, where);\n}\n\ntest(\"sysadmin manages access apps and model channels without exposing secrets\", async ({ page }) => {\n await login(page);\n await expect(page.locator(\".role-select button.on\")).toContainText(\"系统管理员\");\n await gotoScreen(page, \"接入\");\n\n const suffix = Math.random().toString(36).slice(2, 7);\n const appName = `自动化接入应用${suffix}`;\n const channelName = `自动化模型渠道${suffix}`;\n\n await page.getByRole(\"button\", { name: \"新建接入应用\" }).click();\n await page.getByLabel(\"应用名\").fill(appName);\n await page.getByLabel(\"剩余配额\").fill(\"180000\");\n await page.getByLabel(\"L3\").check();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·新建\");\n\n const appRow = page.locator(\"tr\", { hasText: appName });\n await appRow.getByRole(\"button\", { name: \"改配额\" }).click();\n await page.getByLabel(\"剩余配额\").fill(\"220000\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"220000\");\n await assertNoSecretsOrPatientPhi(page, \"接入应用·改配额\");\n\n await appRow.getByRole(\"button\", { name: \"数据等级\" }).click();\n await page.getByLabel(\"L3\").uncheck();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"L2\");\n await assertNoSecretsOrPatientPhi(page, \"接入应用·改数据等级\");\n\n await appRow.getByRole(\"button\", { name: \"停用\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(appRow).toContainText(\"停用\");\n\n await appRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(appName);\n await assertNoSecretsOrPatientPhi(page, \"接入应用·删除\");\n\n await page.locator('.access-tabs button:has-text(\"模型与渠道\")').click();\n await page.getByRole(\"button\", { name: \"新建渠道\" }).click();\n await page.getByLabel(\"渠道名称\").fill(channelName);\n await page.getByLabel(\"类型\").fill(\"openai\");\n await page.getByLabel(\"权重\").fill(\"60\");\n await page.getByLabel(\"模型\").fill(`model-${suffix}`);\n await page.getByLabel(\"服务地址\").fill(\"https://gateway.example.test/v1\");\n await page.getByLabel(\"密钥\").fill(`sk-test-${suffix}`);\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·新建\");\n\n const channelRow = page.locator(\"tr\", { hasText: channelName });\n await channelRow.getByRole(\"button\", { name: \"编辑\" }).click();\n await page.getByLabel(\"权重\").fill(\"55\");\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(channelRow).toContainText(\"55%\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·编辑\");\n\n await channelRow.getByRole(\"button\", { name: \"测试渠道\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n // The probe runs against an unreachable placeholder upstream → honest \"不可达\" verdict\n // (the operation succeeds; the modal stays open showing the result, then closes on 关闭).\n await expect(page.locator(\".access-test-result\")).toContainText(\"不可达\");\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·测试\");\n await page.getByRole(\"button\", { name: \"关闭\" }).click();\n await expect(page.locator(\".access-modal\")).toHaveCount(0);\n\n await channelRow.getByRole(\"button\", { name: \"删除\" }).click();\n await page.getByRole(\"button\", { name: \"确认\" }).click();\n await expect(page.locator(\".dtable\")).not.toContainText(channelName);\n await assertNoSecretsOrPatientPhi(page, \"模型与渠道·删除\");\n});\n"}}}}