From 7b5a90a104a61272dfdfc2071a2943ed165ffdf0 Mon Sep 17 00:00:00 2001 From: QiuSW Date: Mon, 6 Jul 2026 20:54:23 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20T-001=20?= =?UTF-8?q?Go=20=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.yaml.example | 25 ++++++++++++++++ config/config.go | 68 +++++++++++++++++++++++++++++++++++++++++++ config/config_test.go | 20 +++++++++++++ docs/05-实施路线图.md | 3 +- docs/current-state.md | 44 ++++++++++++++++------------ go.mod | 22 ++++++++++++++ go.sum | 52 +++++++++++++++++++++++++++++++++ main.go | 47 ++++++++++++++++++++++++++++++ main_test.go | 21 +++++++++++++ progress.md | 9 ++++++ tasks.md | 4 ++- 11 files changed, 295 insertions(+), 20 deletions(-) create mode 100644 config.yaml.example create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..ac72de0 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,25 @@ +# 占位示例,勿填写真实凭据后提交。 +osi: + base_url: "http://" + org_code: "" + user_name: "" + ask: "" + device_sn: "" + operate_user: "" + timeout_sec: 20 + socks5_proxy: "" + +phis: + base_url: "http://" + token: "" + region_code: "" + poll_interval_sec: 5 + +redis: + addr: "" + db: 0 + +report_log_file_dir: "logs" +retry_max: 3 +circuit_fail_threshold: 5 +circuit_sleep_sec: 60 diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..43c7a13 --- /dev/null +++ b/config/config.go @@ -0,0 +1,68 @@ +package config + +import "github.com/spf13/viper" + +type Config struct { + OSI OSIConfig `mapstructure:"osi"` + PHIS PHISConfig `mapstructure:"phis"` + Redis RedisConfig `mapstructure:"redis"` + ReportLogFileDir string `mapstructure:"report_log_file_dir"` + RetryMax int `mapstructure:"retry_max"` + CircuitFailThreshold int `mapstructure:"circuit_fail_threshold"` + CircuitSleepSec int `mapstructure:"circuit_sleep_sec"` +} + +type OSIConfig struct { + BaseURL string `mapstructure:"base_url"` + OrgCode string `mapstructure:"org_code"` + UserName string `mapstructure:"user_name"` + Ask string `mapstructure:"ask"` + DeviceSN string `mapstructure:"device_sn"` + OperateUser string `mapstructure:"operate_user"` + TimeoutSec int `mapstructure:"timeout_sec"` + Socks5Proxy string `mapstructure:"socks5_proxy"` +} + +type PHISConfig struct { + BaseURL string `mapstructure:"base_url"` + Token string `mapstructure:"token"` + RegionCode string `mapstructure:"region_code"` + PollIntervalSec int `mapstructure:"poll_interval_sec"` +} + +type RedisConfig struct { + Addr string `mapstructure:"addr"` + DB int `mapstructure:"db"` +} + +func Load(path string) (Config, error) { + v := viper.New() + setDefaults(v) + + if path != "" { + v.SetConfigFile(path) + v.SetConfigType("yaml") + if err := v.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); !ok { + return Config{}, err + } + } + } + + var cfg Config + if err := v.Unmarshal(&cfg); err != nil { + return Config{}, err + } + return cfg, nil +} + +func setDefaults(v *viper.Viper) { + v.SetDefault("osi.base_url", "http://") + v.SetDefault("osi.timeout_sec", 20) + v.SetDefault("phis.poll_interval_sec", 5) + v.SetDefault("redis.db", 0) + v.SetDefault("report_log_file_dir", "logs") + v.SetDefault("retry_max", 3) + v.SetDefault("circuit_fail_threshold", 5) + v.SetDefault("circuit_sleep_sec", 60) +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..955c051 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,20 @@ +package config + +import "testing" + +func TestLoadExampleConfig(t *testing.T) { + cfg, err := Load("../config.yaml.example") + if err != nil { + t.Fatalf("Load example config: %v", err) + } + + if cfg.OSI.BaseURL == "" { + t.Fatal("OSI base_url should have a placeholder value") + } + if cfg.OSI.TimeoutSec != 20 { + t.Fatalf("OSI timeout_sec = %d, want 20", cfg.OSI.TimeoutSec) + } + if cfg.ReportLogFileDir != "logs" { + t.Fatalf("report_log_file_dir = %q, want logs", cfg.ReportLogFileDir) + } +} diff --git a/docs/05-实施路线图.md b/docs/05-实施路线图.md index 092d50e..9a38228 100644 --- a/docs/05-实施路线图.md +++ b/docs/05-实施路线图.md @@ -6,7 +6,7 @@ ## 阶段 0 · 脚手架与契约骨架 -- [ ] 初始化 Go module、单 `main.go`(`-mode server|deliver`)、`config/`(viper)。 +- [x] 初始化 Go module、单 `main.go`(`-mode server|deliver`)、`config/`(viper)。 - [ ] 落地 `osi/sign.go`(MD5 签名)+ 单测:用文档约定的 `ts/ask` 校验 `password` 形态(32 位小写)。 - [ ] 移植旧项目 `transport.go`/`http_client.go` → `osi/transport.go`(保留 SOCKS5/超时,去 cookiejar 与拟态头)。 - [ ] `osi/client.go` 的 `Call(serviceId, body, out)`:注入头+信封+发送+判码(成功码实测 `"01"`,按去前导零判定;`405` 可重试,见 docs/01 §1)。 @@ -87,3 +87,4 @@ - 映射层:**与旧项目相当或略增**(但从"逆向猜"变为"照文档写",更确定、更可测)。 - 投递流水线:**基本复用**旧项目经验,少量适配。 - 净效果:总复杂度显著下降,且代码意图清晰可交接。 + diff --git a/docs/current-state.md b/docs/current-state.md index 5622fa7..8d06e9c 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -6,25 +6,28 @@ ## 当前快照 - 日期:2026-07-06 -- 阶段:**pre-code**(设计完成、Go 骨架未建立;路线图阶段 0 未开始) -- 技术栈:目标 Go 1.24 单二进制(未落地);当前仅有 Python 联调脚本(requests,本地不入库) -- 生产代码:**无**。`go.mod` 不存在,`osi/ contract/ mapping/` 等目录均待建 +- 阶段:**Phase 0 已起步**;T-001 已完成,Go module 与 CLI/config 骨架已建立 +- 技术栈:Go 1.24 单二进制;`main.go -mode server|deliver`;配置读取使用 viper +- 生产代码:已有 `main.go`、`config/`、`go.mod`/`go.sum`;`osi/ contract/ mapping/` 等业务目录仍待后续任务建立 - 联调现实:**JKDA00002 个人档案查询已用 Python 脚本打通真实沙箱**(`code="01"`),实测契约沉淀在 `docs/04 §8` -- 测试:`tests/test_query_health_record.py` **已失效**(测的是被重写前的脚本 API,import 报错) -- 标准启动路径:`./init.sh`(阶段 0 未完成前会提示先做 T-001,属预期行为) -- 标准验证路径:`go test ./...`(骨架建立后生效) +- 测试:`go test ./...` 通过;当前测试覆盖 mode 解析与 `config.yaml.example` 加载 +- 标准启动路径:`./init.sh`(需要 bash/WSL 环境;当前 Windows 环境未安装 WSL,直接运行会失败) +- 标准验证路径:`go test ./...`、`go build ./...` - 当前 blocker:无硬 blocker。软阻塞:厂家侧 B1/B2/B4/B5 契约缺口(见 `docs/06`),只影响阶段 4,不阻塞阶段 0~3 ## 当前目录要点 | 路径 | 状态 | 说明 | | --- | --- | --- | +| `main.go` | 已有 | 单入口,解析 `-mode server|deliver` 与 `-config` | +| `config/` | 已有 | viper 配置加载,含最小单测 | +| `config.yaml.example` | 已有 | 占位配置,不含真实凭据 | +| `go.mod` `go.sum` | 已有 | module `chis_osi`,依赖 viper | | `docs/` | 已有 | 设计文档集 01~06 + 本快照;`账号.txt` 本地留存不入库 | | `tasks.md` `progress.md` | 已有 | 任务看板 / 执行流水(根目录) | -| `scripts/` | 已有·不入库 | Python 联调脚本(硬编码真实凭据与身份证,已 gitignore) | -| `tests/` | 已有·失效 | 旧脚本测试,待 T-000 处理 | -| `config.yaml` | 已有·不入库 | 真实凭据;`config.yaml.example` 待 T-001 建立 | -| `main.go` `osi/` `contract/` `mapping/` 等 | 待建 | 路线图阶段 0~2 | +| `scripts/` | 已有·不入库 | Python 联调脚本(硬编码真实凭据与身份证,勿提交) | +| `config.yaml` | 已有·不入库 | 真实凭据;不要提交 | +| `osi/` `contract/` `mapping/` 等 | 待建 | 路线图阶段 0~2 后续任务 | ## 已验证事实(写代码时直接依赖) @@ -37,19 +40,24 @@ ## 当前可运行内容 ```bash -# 本地联调查询(唯一可运行路径;脚本硬编码凭据,勿提交) -python3 scripts/query_health_record.py - -# Go 侧(阶段 0 后) -./init.sh +# Go 骨架验证 go test ./... +go build ./... +go run . -mode server -config config.yaml.example +go run . -mode deliver -config config.yaml.example + +# 统一入口(需要 bash/WSL) +./init.sh + +# 本地联调查询(脚本硬编码凭据,勿提交) +python3 scripts/query_health_record.py ``` ## 下一步 -1. T-000:处理失效测试(`tasks.md` Phase B)。 -2. T-001:go module + 双子命令骨架,进入路线图阶段 0。 +1. T-002:`osi/sign.go` MD5 头签名 + 单测。 +2. T-003:`osi/transport.go` 传输层骨架。 ## 维护规则 -发生以下变化时覆盖更新本文:入口/目录变动、任务状态变化、新增可运行命令、发现文档与代码现实不一致。本文只留当前快照,不留历史。 +发生以下变化时覆盖更新本文:入口/目录变动、任务状态变化、新增可运行命令、发现文档与代码现实不一致。本文只留当前快照,不留历史。 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..08293df --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module chis_osi + +go 1.24 + +require github.com/spf13/viper v1.20.1 + +require ( + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ef6bae --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..99cd73a --- /dev/null +++ b/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "chis_osi/config" +) + +type appMode string + +const ( + modeServer appMode = "server" + modeDeliver appMode = "deliver" +) + +func parseMode(raw string) (appMode, error) { + switch appMode(raw) { + case modeServer: + return modeServer, nil + case modeDeliver: + return modeDeliver, nil + default: + return "", fmt.Errorf("invalid mode %q, want server or deliver", raw) + } +} + +func main() { + modeFlag := flag.String("mode", string(modeServer), "运行模式:server 或 deliver") + configPath := flag.String("config", "config.yaml", "配置文件路径") + flag.Parse() + + mode, err := parseMode(*modeFlag) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + + _, err = config.Load(*configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "load config: %v\n", err) + os.Exit(2) + } + + fmt.Printf("chis_osi mode=%s\n", mode) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..4438dbd --- /dev/null +++ b/main_test.go @@ -0,0 +1,21 @@ +package main + +import "testing" + +func TestParseModeAcceptsServerAndDeliver(t *testing.T) { + for _, raw := range []string{"server", "deliver"} { + mode, err := parseMode(raw) + if err != nil { + t.Fatalf("parseMode(%q) returned error: %v", raw, err) + } + if string(mode) != raw { + t.Fatalf("parseMode(%q) = %q", raw, mode) + } + } +} + +func TestParseModeRejectsUnknownMode(t *testing.T) { + if _, err := parseMode("worker"); err == nil { + t.Fatal("parseMode accepted unknown mode") + } +} diff --git a/progress.md b/progress.md index 34afc1a..fea1e9c 100644 --- a/progress.md +++ b/progress.md @@ -48,3 +48,12 @@ - 验证:`git status --short` 确认删除前后均无该目录的跟踪历史;删除后工作区干净。 - 决策:不改写为新契约的最小测试,直接删除——脚本是硬编码的本地联调工具(不入库),暂不需要自动化测试覆盖。 - 下一步:T-001,进入路线图阶段 0(go module + 双子命令骨架)。 + +## 2026-07-06 T-001 初始化 Go module 与双子命令骨架 + +- 状态:DONE +- 变更:新增 `go.mod`/`go.sum`、`main.go`、`main_test.go`、`config/config.go`、`config/config_test.go`、`config.yaml.example`;`tasks.md` 标记 T-001 完成;`docs/05` 勾选阶段 0 第一项。 +- 验证:`go test ./...` 通过;`go build ./...` 通过;`go run . -mode server -config config.yaml.example` 输出 `chis_osi mode=server`;`go run . -mode deliver -config config.yaml.example` 输出 `chis_osi mode=deliver`。 +- 验证补充:`bash init.sh` 在当前 Windows 环境失败,系统提示未安装 WSL;已用等价 Go 命令完成验收。 +- 决策:Go module 使用本地模块名 `chis_osi`;`main.go` 只打印 mode,不打印配置值,避免泄露真实环境信息;`config.yaml.example` 只保留占位值。 +- 下一步:T-002(`osi/sign.go` MD5 头签名 + 单测)。 diff --git a/tasks.md b/tasks.md index 0f709bb..bb521eb 100644 --- a/tasks.md +++ b/tasks.md @@ -25,7 +25,7 @@ | ID | 任务 | 依赖 | 验收要点 | 状态 | | --- | --- | --- | --- | --- | -| T-001 | 初始化 go module + `main.go` 双子命令骨架 + `config/`(viper) | - | `go build ./...` 通过;`-mode server\|deliver` 可解析;`config.yaml.example` 占位值就绪 | TODO | +| T-001 | 初始化 go module + `main.go` 双子命令骨架 + `config/`(viper) | - | `go build ./...` 通过;`-mode server\|deliver` 可解析;`config.yaml.example` 占位值就绪 | DONE | | T-002 | `osi/sign.go` MD5 头签名 + 单测 | T-001 | 单测校验 `password=md5("ts=&ask=")` 32 位小写;与 Python 脚本(scripts/,本地)产出比对一致 | TODO | | T-003 | `osi/transport.go`:从 chis_upload 移植传输层 | T-001 | 保留 SOCKS5/超时;去掉 cookiejar 与网页拟态头;单测或最小连通验证 | TODO | | T-004 | `osi/client.go` `Call` + `osi/codes.go` + `contract/envelope.go` | T-002, T-003 | 信封为 `serviceId`+`uploadinfo{baseInfo,manageInfo,...}`(docs/04 §8);成功码按去前导零 == `"1"` 判定(实测 `"01"`,docs/01 §1);`405` 归类可重试 | TODO | @@ -62,3 +62,5 @@ - 阶段 5:PHIS 真实接入与状态回写。 - 阶段 6:加固与交接(完整度定论、密钥环境变量化、运维文档、全绿)。 - 联调依赖跟踪见 `docs/06-厂家联调清单.md`(B 组契约缺口会阻塞阶段 4)。 + +