2
0
Эх сурвалжийг харах

fix: wp_login.py 在非交互终端里明确报错,并补 --password-stdin

设计文档 §6.2 原本写「在 Claude Code 中用 ! 前缀运行 wp_login.py」,
2026-08-05 实测推翻:! 前缀跑的命令没有交互式终端,getpass 的提示无处
输入,用户只会看到一个静默失败。

- 非交互且未给 --password-stdin 时直接报错并指路(另开真实终端 / 用管道),
  不再让用户对着提示符发愣;
- 新增 --password-stdin 从管道读密码,供自动化用。密码仍不能经 argv
  (进 ps 与 shell history),也不能打进对话(进上下文);
- SKILL.md 与设计文档同步纠正。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
visuddhinanda 1 долоо хоног өмнө
parent
commit
4162b27e0f

+ 5 - 4
.claude/skills/wikipali-write/SKILL.md

@@ -18,8 +18,9 @@ metadata:
 
 ## 铁律
 
-1. **永远不要向用户索要密码,也不要代跑 `wp_login.py`。** 需要登录时,请用户自己在 Claude Code 里执行:
-   `! python3 .claude/skills/wikipali-write/scripts/wp_login.py`
+1. **永远不要向用户索要密码,也不要代跑 `wp_login.py`。** 需要登录时,请用户**另开一个真正的终端**执行
+   `python3 .claude/skills/wikipali-write/scripts/wp_login.py`。
+   不要让他们用 Claude Code 的 `!` 前缀——那里没有交互式终端,密码提示无处输入;也不要建议把密码放进命令行参数或直接打在对话里。
 2. **写入前必须让用户确认。** `wp.py write` 默认会回显目标并等确认;只有用户已经明确同意本次写入时,才可以加 `-y`。
 3. **绝不打印 token 全文**(`~/.wikipali/credentials.json` 里的任何值)。脚本自己会打码,不要 `cat` 那个文件。
 4. **`count` 不等于提交条数就是有句子没写进去**,必须如实报告给用户,不要说「已全部写入」。
@@ -34,8 +35,8 @@ python3 scripts/wp.py whoami          # 先看缺什么
 按缺什么补什么:
 
 ```bash
-# 1) 登录(用户自己跑,带 ! 前缀
-! python3 .claude/skills/wikipali-write/scripts/wp_login.py
+# 1) 登录(用户自己在另一个终端里跑,不要用 ! 前缀,也不要代跑
+python3 .claude/skills/wikipali-write/scripts/wp_login.py
 
 # 2) 建立模型身份并取 token;--name 必须是你自己的模型标识
 python3 scripts/wp.py ensure-model --name claude-opus-5

+ 35 - 9
.claude/skills/wikipali-write/scripts/wp_login.py

@@ -9,8 +9,15 @@
     python3 wp_login.py --api next            # 只为本次登录换站点
     python3 wp_login.py --username someone    # 免去输用户名一步
 
-在 Claude Code 中请用 `!` 前缀由用户本人执行,不要让模型代跑:
-    ! python3 .claude/skills/wikipali-write/scripts/wp_login.py
+**必须在真正的终端里跑。** Claude Code 的 `!` 前缀没有交互式终端,
+密码提示无处输入;模型也不该代跑此脚本。请另开一个 shell 执行。
+
+确实要在自动化环境里登录时,用 --password-stdin 从管道读密码:
+
+    read -rs PW && printf '%s' "$PW" | python3 wp_login.py --username me --password-stdin
+
+注意别把密码写进命令行参数或直接敲进 Claude Code 的会话——argv 会进
+ps / shell history,会话内容会进对话上下文,两者都留痕。
 """
 
 import argparse
@@ -24,6 +31,10 @@ def main(argv=None):
     parser = argparse.ArgumentParser(prog="wp_login.py", description="登录 WikiPali 并缓存用户 token")
     parser.add_argument("--api", help="本次登录使用的 API 地址(序号/简称/完整 url)")
     parser.add_argument("--username", help="用户名或邮箱;省略则交互输入")
+    parser.add_argument(
+        "--password-stdin", action="store_true",
+        help="从 stdin 读密码(供自动化用;别让密码经过 argv 或对话)",
+    )
     args = parser.parse_args(argv)
 
     try:
@@ -36,21 +47,36 @@ def main(argv=None):
     if client.bucket_name != "online":
         print("注意:该站点的凭据与线上四站不通用。")
 
+    interactive = sys.stdin.isatty()
+    if not interactive and not args.password_stdin:
+        # Claude Code 的 `!` 前缀、CI、管道都会走到这里:密码提示无处输入,
+        # 直接说清楚该怎么办,不要让用户对着一个静默的提示符发愣
+        print(
+            "错误:当前不是交互式终端,无法安全地读取密码。\n"
+            "  · 请另开一个真正的终端执行本脚本(Claude Code 的 `!` 前缀不行);\n"
+            "  · 或在自动化环境里用管道:... | python3 wp_login.py --username <名字> --password-stdin",
+            file=sys.stderr,
+        )
+        return 1
+
     username = args.username
     if not username:
-        if not sys.stdin.isatty():
-            print("错误:非交互式环境请用 --username 指定用户名。", file=sys.stderr)
+        if not interactive:
+            print("错误:--password-stdin 模式必须同时给 --username。", file=sys.stderr)
             return 1
         username = input("用户名或邮箱:").strip()
     if not username:
         print("错误:用户名为空。", file=sys.stderr)
         return 1
 
-    try:
-        password = getpass.getpass("密码(不会被保存):")
-    except (EOFError, KeyboardInterrupt):
-        print("\n已取消。", file=sys.stderr)
-        return 130
+    if args.password_stdin:
+        password = sys.stdin.readline().rstrip("\n")
+    else:
+        try:
+            password = getpass.getpass("密码(不会被保存):")
+        except (EOFError, KeyboardInterrupt):
+            print("\n已取消。", file=sys.stderr)
+            return 130
     if not password:
         print("错误:密码为空。", file=sys.stderr)
         return 1

+ 2 - 1
docs/wikipali-write-skill-design.md

@@ -429,7 +429,8 @@ $ python3 scripts/wp.py endpoint next
 只有 `online` / `local` 两桶,理由见 §6.1.2:四个线上地址共享库与密钥,凭据通用。`online.api_url` 只记「上次选的是哪个地址」,换地区或在 www / next 之间切换就是改这一个字段,`user` / `model` / `access_tokens` 全部原样沿用。`local` 单独一桶是因为开发机是另一个库、另一把 `jwt_secrets_key`。
 
 原则:
-- **Claude 永不接触明文密码**。登录由用户自己执行 `python scripts/wp_login.py`(`getpass` 读取),或在 Claude Code 中用 `! python .../wp_login.py` 前缀运行;
+- **Claude 永不接触明文密码**。登录由用户自己在**一个真正的终端**里执行 `python3 scripts/wp_login.py`(`getpass` 读取)。
+  ~~或在 Claude Code 中用 `! python .../wp_login.py` 前缀运行~~——2026-08-05 实测推翻:`!` 前缀跑的命令没有交互式终端(`sys.stdin.isatty()` 为假),密码提示无处输入。脚本会明确报错并指路,另提供 `--password-stdin` 供自动化场景从管道读取;密码**不能**经 argv 传(进 `ps` 与 shell history),也不能直接打进对话(进上下文);
 - Skill 读取凭据文件时只取 token,不回显到对话中(日志里 token 一律打码);
 - 任一 token 收到 401 → 提示重新登录,而不是自动重试;
 - 缓存的 `model.token` 有效期只有 30 天,且可能被 owner 主动撤销(§2.3b)。两种情况的表现都是 401,处理一致:重跑 §6.3 第 4 步重取,仍 401 才提示重新登录。