procproxy 0.1.1__tar.gz → 0.1.2__tar.gz

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.
Files changed (53) hide show
  1. {procproxy-0.1.1/src/procproxy.egg-info → procproxy-0.1.2}/PKG-INFO +144 -14
  2. {procproxy-0.1.1 → procproxy-0.1.2}/README.md +143 -13
  3. {procproxy-0.1.1 → procproxy-0.1.2}/pyproject.toml +1 -1
  4. {procproxy-0.1.1 → procproxy-0.1.2/src/procproxy.egg-info}/PKG-INFO +144 -14
  5. {procproxy-0.1.1 → procproxy-0.1.2}/src/procproxy.egg-info/SOURCES.txt +1 -0
  6. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/__init__.py +1 -1
  7. procproxy-0.1.2/src/proxycore/api.py +315 -0
  8. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/canproxy.py +22 -20
  9. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/checkroute.py +14 -10
  10. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/cli.py +56 -72
  11. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/config.py +28 -6
  12. procproxy-0.1.2/src/proxycore/config_source.py +154 -0
  13. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/service/__init__.py +11 -2
  14. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/service/__main__.py +19 -5
  15. procproxy-0.1.1/src/proxycore/api.py +0 -226
  16. {procproxy-0.1.1 → procproxy-0.1.2}/LICENSE +0 -0
  17. {procproxy-0.1.1 → procproxy-0.1.2}/setup.cfg +0 -0
  18. {procproxy-0.1.1 → procproxy-0.1.2}/src/procproxy.egg-info/dependency_links.txt +0 -0
  19. {procproxy-0.1.1 → procproxy-0.1.2}/src/procproxy.egg-info/entry_points.txt +0 -0
  20. {procproxy-0.1.1 → procproxy-0.1.2}/src/procproxy.egg-info/requires.txt +0 -0
  21. {procproxy-0.1.1 → procproxy-0.1.2}/src/procproxy.egg-info/top_level.txt +0 -0
  22. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/__main__.py +0 -0
  23. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/__init__.py +0 -0
  24. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/base.py +0 -0
  25. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/null.py +0 -0
  26. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/windivert/__init__.py +0 -0
  27. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/windivert/filter.py +0 -0
  28. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/windivert/pidmap.py +0 -0
  29. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/backend/windivert/pkt.py +0 -0
  30. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/data/default.yaml +0 -0
  31. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/dns/__init__.py +0 -0
  32. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/dns/cache.py +0 -0
  33. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/dns/modes.py +0 -0
  34. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/dns/udp.py +0 -0
  35. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/forward.py +0 -0
  36. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/ipc.py +0 -0
  37. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/log.py +0 -0
  38. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/pathsetup.py +0 -0
  39. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/probe.py +0 -0
  40. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/proc.py +0 -0
  41. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/rules.py +0 -0
  42. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/runtime.py +0 -0
  43. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/tunnel/__init__.py +0 -0
  44. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/tunnel/relay.py +0 -0
  45. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/tunnel/sessions.py +0 -0
  46. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/tunnel/sink.py +0 -0
  47. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/types.py +0 -0
  48. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/__init__.py +0 -0
  49. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/base.py +0 -0
  50. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/chain.py +0 -0
  51. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/http_connect.py +0 -0
  52. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/socks4.py +0 -0
  53. {procproxy-0.1.1 → procproxy-0.1.2}/src/proxycore/upstream/socks5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procproxy
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: ProcProxy —— 进程级代理:按进程决定哪条连接走哪个上游代理
5
5
  Author: Longersking
6
6
  License-Expression: MIT
@@ -78,6 +78,8 @@ procproxy forward WeChat.exe --port 8848 # 真实转发(需管理员)
78
78
  ```
79
79
 
80
80
  安装后直接运行 `procproxy` 会显示首次使用指引,不依赖任何样例文件。
81
+ Python SDK 也一样:`start_proxy()` 不传配置即可启动(内置 `backend=null`),
82
+ 不需要先跑 `init`;`init` 只用于生成可编辑配置。
81
83
 
82
84
  非管理员:WinDivert 抓包必须管理员;可以用 `--backend null` 做转发链路联调,
83
85
  或在服务已启动、且配置了 `service.ipc_allow_group` 时读取状态。
@@ -344,24 +346,151 @@ rules: # 自上而下,**首个命中生
344
346
 
345
347
  ## 编程接口
346
348
 
349
+ `proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
350
+ 完整教程见 [`docs/15-编程接口与SDK.md`](docs/15-编程接口与SDK.md)。
351
+
352
+ ### 30 秒:零配置启动
353
+
354
+ ```python
355
+ from proxycore.api import start_proxy
356
+
357
+ with start_proxy() as ppx: # 不需要任何配置文件
358
+ print(ppx.config_source) # "packaged"
359
+ print(ppx.config_path) # .../proxycore/data/default.yaml
360
+ print(ppx.status()) # 后端 / 规则 / 会话 / UDP / 配置来源
361
+ ```
362
+
363
+ 不传配置时的来源优先级:
364
+
365
+ | 优先级 | 来源 | 说明 |
366
+ |---|---|---|
367
+ | 1 | `start_proxy(path)` | 显式路径(也接受内存中的 `Config` 对象) |
368
+ | 2 | `PROCPROXY_CONFIG` | 指向不存在文件会直接报错,不回退 |
369
+ | 3 | `~/.procproxy/config.yaml` | `procproxy init` 生成的用户配置 |
370
+ | 4 | 包内 `data/default.yaml` | `backend=null`,不抓包、不改道 |
371
+
372
+ 内置配置**不会**监听 sink,也**不会**创建任何文件。它适合装配、规则决策、
373
+ 状态查询和联调;要真实抓包请用 `backend="windivert"`(需管理员)或
374
+ `procproxy init` 生成自己的配置。
375
+
376
+ ### 显式配置与测试转发
377
+
347
378
  ```python
348
- from proxycore import api
379
+ from proxycore.api import start_proxy
349
380
 
350
- # 上下文管理器:退出自动停
351
- with api.start_proxy("samples/profiles/dev.yaml") as handle:
352
- print(handle.status())
381
+ # 显式路径 + 真实抓包(需要管理员)
382
+ with start_proxy(r"D:\cfg\procproxy.yaml", backend="windivert") as ppx:
383
+ print(ppx.sink) # sink 监听地址
384
+ print(ppx.sessions(limit=20))
353
385
 
354
- # 测试模式:sink 把所有连接转发到固定目标(不做抓包)
355
- with api.start_proxy("samples/profiles/dev.yaml", backend="null",
356
- sink_test_target="127.0.0.1:8080") as handle:
386
+ # 联调模式:backend=null + 把 sink 收到的连接固定转发到目标
387
+ with start_proxy(backend="null", sink_test_target="127.0.0.1:8080",
388
+ upstream="local-mock") as ppx:
357
389
  ...
390
+ # curl -x http://127.0.0.1:19080 http://any/ → 127.0.0.1:8080
391
+ ```
392
+
393
+ ### 在代码里构建配置
394
+
395
+ 可以从字典构造 `Config`,也可以先落盘再按路径加载:
396
+
397
+ ```python
398
+ from proxycore.config import Config, atomic_write
399
+ from proxycore.api import start_proxy
400
+
401
+ doc = {
402
+ "version": 1,
403
+ "service": {"sink": {"host": "127.0.0.1", "port": 19080}, "backend": "null"},
404
+ "profile": {
405
+ "name": "myapp",
406
+ "default_action": "direct",
407
+ "dns_mode": "remote",
408
+ "rules": [
409
+ {"id": "r1", "action": "proxy", "upstream": "office",
410
+ "targets": "chrome.exe;msedge.exe", "dst_port": "443,80"},
411
+ ],
412
+ },
413
+ "upstreams": [
414
+ {"alias": "office", "kind": "socks5", "host": "127.0.0.1", "port": 1080},
415
+ ],
416
+ }
417
+
418
+ cfg = Config.from_dict(doc)
419
+ cfg.validate() # from_dict 只解析,不校验
420
+ atomic_write(cfg, "myconfig.yaml") # 临时文件 → 校验 → 原子替换
421
+
422
+ with start_proxy("myconfig.yaml") as ppx:
423
+ ...
424
+ ```
425
+
426
+ > `Config.from_dict()` 不会解析 `password_ref`;需要凭据时请用
427
+ > `Config.load()` / `start_proxy(path)`,或自己给 `upstream.password` 赋值。
358
428
 
359
- # 只要规则决策,不要监听
360
- runtime = api.make_runtime("samples/profiles/dev.yaml", backend="null")
429
+ ### 只做决策,不启动数据面
430
+
431
+ ```python
432
+ from proxycore.api import make_runtime
433
+
434
+ runtime = make_runtime() # 默认用内置配置,backend=null
361
435
  decision = runtime.decide(conn_request)
436
+ print(decision.action, decision.reason)
437
+ ```
438
+
439
+ ### 生命周期与句柄
440
+
441
+ ```python
442
+ ppx = start_proxy()
443
+ try:
444
+ print(ppx.status())
445
+ finally:
446
+ ppx.stop() # 幂等;with 退出时也会自动调用
447
+ ```
448
+
449
+ | 成员 | 说明 |
450
+ |---|---|
451
+ | `status()` | 后端、规则、会话、熔断、DNS、UDP、sink、`config_path`、`config_source` |
452
+ | `sessions(limit=50)` / `active_sessions()` | 会话明细 / 活跃数 |
453
+ | `reload(path="")` | 热重载;不传路径则重读当前文件。`sink.port`/`backend` 变更需重启 |
454
+ | `decide(conn)` | 只问规则引擎,不抓包、不改道 |
455
+ | `profile` | 当前生效的 `Profile` |
456
+ | `config_path` / `config_source` | 当前配置的绝对路径与来源 |
457
+ | `running` | 是否仍在运行 |
458
+
459
+ `start_proxy()` 会启动后台维护线程;`run_forever()` 会阻塞到 Ctrl+C。
460
+ 请在主线程使用,并在退出时 `stop()`(或使用 `with`)。
461
+
462
+ ### 错误处理
463
+
464
+ 配置 / 后端 / 启动失败统一抛 `ProxyStartError`,`code` 是稳定的机器可读标识:
465
+
466
+ ```python
467
+ from proxycore.api import ProxyStartError, start_proxy
468
+
469
+ try:
470
+ start_proxy("missing.yaml")
471
+ except ProxyStartError as exc:
472
+ print(exc.code) # config_not_found
362
473
  ```
363
474
 
364
- 跑 `python tests/tools/api_demo.py` 看三种用法的完整演示。
475
+ | code | 含义 |
476
+ |---|---|
477
+ | `config_not_found` | 配置文件不存在(显式路径 / 环境变量不会回退) |
478
+ | `config_unreadable` | 文件存在但读不了(权限、目录等) |
479
+ | `config_parse_invalid` | YAML 语法错误 / 根节点非映射 / 非 UTF-8 |
480
+ | `config_version_unsupported` | `version != 1` |
481
+ | `rule_upstream_missing` 等 | 规则或上游校验失败(完整列表见文档) |
482
+ | `E_BACKEND_UNAVAILABLE` | 后端不可用(未知后端、WinDivert 不可加载等) |
483
+
484
+ `api.load_config()` 同样抛 `ProxyStartError`,不是 `ConfigError`。
485
+
486
+ ### 环境变量
487
+
488
+ | 变量 | 作用 |
489
+ |---|---|
490
+ | `PROCPROXY_CONFIG` | 配置文件路径;显式路径之后、用户配置之前生效 |
491
+ | `PROCPROXY_RUNTIME_DIR` | 运行时状态/日志目录(CLI 与服务使用) |
492
+
493
+ 跑 `python tests/tools/api_demo.py` 看完整演示。
365
494
 
366
495
  ---
367
496
 
@@ -406,12 +535,13 @@ src/proxycore/ 核心包
406
535
  ├── tunnel/ sink + relay(本地落地与双向转发)
407
536
  ├── upstream/ SOCKS5 / SOCKS4 / HTTP CONNECT / 跳链
408
537
  ├── dns/ DNS 三模式与 UDP
538
+ ├── config_source.py 配置来源解析(显式 / env / 用户 / 样例 / 内置)
409
539
  ├── forward.py ← procproxy.py <进程名> 的实现
410
540
  ├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
411
541
  └── service/ ipc.py Windows 服务 + 命名管道
412
542
  samples/ mock 服务 + 示例配置
413
- docs/ 文档(00 架构与阅读路线 / 07 规则语义 / 11 排错 / 14 转发到抓包工具)
414
- tests/ 测试套件(555 passed)
543
+ docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
544
+ tests/ 测试套件(619 passed / 4 skipped)
415
545
  └── tools/ 验证脚本(run_demo / run_e2e / bench / …)
416
546
  ```
417
547
 
@@ -442,7 +572,7 @@ tests/ 测试套件(555 passed)
442
572
 
443
573
  ```bash
444
574
  pip install -e ".[dev]"
445
- python -m pytest -q tests/ # 555 passed / 4 skipped(非管理员 Windows)
575
+ python -m pytest -q tests/ # 619 passed / 4 skipped(非管理员 Windows)
446
576
  python -m ruff check src tests
447
577
  python -m mypy src/proxycore
448
578
  ```
@@ -39,6 +39,8 @@ procproxy forward WeChat.exe --port 8848 # 真实转发(需管理员)
39
39
  ```
40
40
 
41
41
  安装后直接运行 `procproxy` 会显示首次使用指引,不依赖任何样例文件。
42
+ Python SDK 也一样:`start_proxy()` 不传配置即可启动(内置 `backend=null`),
43
+ 不需要先跑 `init`;`init` 只用于生成可编辑配置。
42
44
 
43
45
  非管理员:WinDivert 抓包必须管理员;可以用 `--backend null` 做转发链路联调,
44
46
  或在服务已启动、且配置了 `service.ipc_allow_group` 时读取状态。
@@ -305,24 +307,151 @@ rules: # 自上而下,**首个命中生
305
307
 
306
308
  ## 编程接口
307
309
 
310
+ `proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
311
+ 完整教程见 [`docs/15-编程接口与SDK.md`](docs/15-编程接口与SDK.md)。
312
+
313
+ ### 30 秒:零配置启动
314
+
315
+ ```python
316
+ from proxycore.api import start_proxy
317
+
318
+ with start_proxy() as ppx: # 不需要任何配置文件
319
+ print(ppx.config_source) # "packaged"
320
+ print(ppx.config_path) # .../proxycore/data/default.yaml
321
+ print(ppx.status()) # 后端 / 规则 / 会话 / UDP / 配置来源
322
+ ```
323
+
324
+ 不传配置时的来源优先级:
325
+
326
+ | 优先级 | 来源 | 说明 |
327
+ |---|---|---|
328
+ | 1 | `start_proxy(path)` | 显式路径(也接受内存中的 `Config` 对象) |
329
+ | 2 | `PROCPROXY_CONFIG` | 指向不存在文件会直接报错,不回退 |
330
+ | 3 | `~/.procproxy/config.yaml` | `procproxy init` 生成的用户配置 |
331
+ | 4 | 包内 `data/default.yaml` | `backend=null`,不抓包、不改道 |
332
+
333
+ 内置配置**不会**监听 sink,也**不会**创建任何文件。它适合装配、规则决策、
334
+ 状态查询和联调;要真实抓包请用 `backend="windivert"`(需管理员)或
335
+ `procproxy init` 生成自己的配置。
336
+
337
+ ### 显式配置与测试转发
338
+
308
339
  ```python
309
- from proxycore import api
340
+ from proxycore.api import start_proxy
310
341
 
311
- # 上下文管理器:退出自动停
312
- with api.start_proxy("samples/profiles/dev.yaml") as handle:
313
- print(handle.status())
342
+ # 显式路径 + 真实抓包(需要管理员)
343
+ with start_proxy(r"D:\cfg\procproxy.yaml", backend="windivert") as ppx:
344
+ print(ppx.sink) # sink 监听地址
345
+ print(ppx.sessions(limit=20))
314
346
 
315
- # 测试模式:sink 把所有连接转发到固定目标(不做抓包)
316
- with api.start_proxy("samples/profiles/dev.yaml", backend="null",
317
- sink_test_target="127.0.0.1:8080") as handle:
347
+ # 联调模式:backend=null + 把 sink 收到的连接固定转发到目标
348
+ with start_proxy(backend="null", sink_test_target="127.0.0.1:8080",
349
+ upstream="local-mock") as ppx:
318
350
  ...
351
+ # curl -x http://127.0.0.1:19080 http://any/ → 127.0.0.1:8080
352
+ ```
353
+
354
+ ### 在代码里构建配置
355
+
356
+ 可以从字典构造 `Config`,也可以先落盘再按路径加载:
357
+
358
+ ```python
359
+ from proxycore.config import Config, atomic_write
360
+ from proxycore.api import start_proxy
361
+
362
+ doc = {
363
+ "version": 1,
364
+ "service": {"sink": {"host": "127.0.0.1", "port": 19080}, "backend": "null"},
365
+ "profile": {
366
+ "name": "myapp",
367
+ "default_action": "direct",
368
+ "dns_mode": "remote",
369
+ "rules": [
370
+ {"id": "r1", "action": "proxy", "upstream": "office",
371
+ "targets": "chrome.exe;msedge.exe", "dst_port": "443,80"},
372
+ ],
373
+ },
374
+ "upstreams": [
375
+ {"alias": "office", "kind": "socks5", "host": "127.0.0.1", "port": 1080},
376
+ ],
377
+ }
378
+
379
+ cfg = Config.from_dict(doc)
380
+ cfg.validate() # from_dict 只解析,不校验
381
+ atomic_write(cfg, "myconfig.yaml") # 临时文件 → 校验 → 原子替换
382
+
383
+ with start_proxy("myconfig.yaml") as ppx:
384
+ ...
385
+ ```
386
+
387
+ > `Config.from_dict()` 不会解析 `password_ref`;需要凭据时请用
388
+ > `Config.load()` / `start_proxy(path)`,或自己给 `upstream.password` 赋值。
319
389
 
320
- # 只要规则决策,不要监听
321
- runtime = api.make_runtime("samples/profiles/dev.yaml", backend="null")
390
+ ### 只做决策,不启动数据面
391
+
392
+ ```python
393
+ from proxycore.api import make_runtime
394
+
395
+ runtime = make_runtime() # 默认用内置配置,backend=null
322
396
  decision = runtime.decide(conn_request)
397
+ print(decision.action, decision.reason)
398
+ ```
399
+
400
+ ### 生命周期与句柄
401
+
402
+ ```python
403
+ ppx = start_proxy()
404
+ try:
405
+ print(ppx.status())
406
+ finally:
407
+ ppx.stop() # 幂等;with 退出时也会自动调用
408
+ ```
409
+
410
+ | 成员 | 说明 |
411
+ |---|---|
412
+ | `status()` | 后端、规则、会话、熔断、DNS、UDP、sink、`config_path`、`config_source` |
413
+ | `sessions(limit=50)` / `active_sessions()` | 会话明细 / 活跃数 |
414
+ | `reload(path="")` | 热重载;不传路径则重读当前文件。`sink.port`/`backend` 变更需重启 |
415
+ | `decide(conn)` | 只问规则引擎,不抓包、不改道 |
416
+ | `profile` | 当前生效的 `Profile` |
417
+ | `config_path` / `config_source` | 当前配置的绝对路径与来源 |
418
+ | `running` | 是否仍在运行 |
419
+
420
+ `start_proxy()` 会启动后台维护线程;`run_forever()` 会阻塞到 Ctrl+C。
421
+ 请在主线程使用,并在退出时 `stop()`(或使用 `with`)。
422
+
423
+ ### 错误处理
424
+
425
+ 配置 / 后端 / 启动失败统一抛 `ProxyStartError`,`code` 是稳定的机器可读标识:
426
+
427
+ ```python
428
+ from proxycore.api import ProxyStartError, start_proxy
429
+
430
+ try:
431
+ start_proxy("missing.yaml")
432
+ except ProxyStartError as exc:
433
+ print(exc.code) # config_not_found
323
434
  ```
324
435
 
325
- 跑 `python tests/tools/api_demo.py` 看三种用法的完整演示。
436
+ | code | 含义 |
437
+ |---|---|
438
+ | `config_not_found` | 配置文件不存在(显式路径 / 环境变量不会回退) |
439
+ | `config_unreadable` | 文件存在但读不了(权限、目录等) |
440
+ | `config_parse_invalid` | YAML 语法错误 / 根节点非映射 / 非 UTF-8 |
441
+ | `config_version_unsupported` | `version != 1` |
442
+ | `rule_upstream_missing` 等 | 规则或上游校验失败(完整列表见文档) |
443
+ | `E_BACKEND_UNAVAILABLE` | 后端不可用(未知后端、WinDivert 不可加载等) |
444
+
445
+ `api.load_config()` 同样抛 `ProxyStartError`,不是 `ConfigError`。
446
+
447
+ ### 环境变量
448
+
449
+ | 变量 | 作用 |
450
+ |---|---|
451
+ | `PROCPROXY_CONFIG` | 配置文件路径;显式路径之后、用户配置之前生效 |
452
+ | `PROCPROXY_RUNTIME_DIR` | 运行时状态/日志目录(CLI 与服务使用) |
453
+
454
+ 跑 `python tests/tools/api_demo.py` 看完整演示。
326
455
 
327
456
  ---
328
457
 
@@ -367,12 +496,13 @@ src/proxycore/ 核心包
367
496
  ├── tunnel/ sink + relay(本地落地与双向转发)
368
497
  ├── upstream/ SOCKS5 / SOCKS4 / HTTP CONNECT / 跳链
369
498
  ├── dns/ DNS 三模式与 UDP
499
+ ├── config_source.py 配置来源解析(显式 / env / 用户 / 样例 / 内置)
370
500
  ├── forward.py ← procproxy.py <进程名> 的实现
371
501
  ├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
372
502
  └── service/ ipc.py Windows 服务 + 命名管道
373
503
  samples/ mock 服务 + 示例配置
374
- docs/ 文档(00 架构与阅读路线 / 07 规则语义 / 11 排错 / 14 转发到抓包工具)
375
- tests/ 测试套件(555 passed)
504
+ docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
505
+ tests/ 测试套件(619 passed / 4 skipped)
376
506
  └── tools/ 验证脚本(run_demo / run_e2e / bench / …)
377
507
  ```
378
508
 
@@ -403,7 +533,7 @@ tests/ 测试套件(555 passed)
403
533
 
404
534
  ```bash
405
535
  pip install -e ".[dev]"
406
- python -m pytest -q tests/ # 555 passed / 4 skipped(非管理员 Windows)
536
+ python -m pytest -q tests/ # 619 passed / 4 skipped(非管理员 Windows)
407
537
  python -m ruff check src tests
408
538
  python -m mypy src/proxycore
409
539
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "procproxy"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "ProcProxy —— 进程级代理:按进程决定哪条连接走哪个上游代理"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: procproxy
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: ProcProxy —— 进程级代理:按进程决定哪条连接走哪个上游代理
5
5
  Author: Longersking
6
6
  License-Expression: MIT
@@ -78,6 +78,8 @@ procproxy forward WeChat.exe --port 8848 # 真实转发(需管理员)
78
78
  ```
79
79
 
80
80
  安装后直接运行 `procproxy` 会显示首次使用指引,不依赖任何样例文件。
81
+ Python SDK 也一样:`start_proxy()` 不传配置即可启动(内置 `backend=null`),
82
+ 不需要先跑 `init`;`init` 只用于生成可编辑配置。
81
83
 
82
84
  非管理员:WinDivert 抓包必须管理员;可以用 `--backend null` 做转发链路联调,
83
85
  或在服务已启动、且配置了 `service.ipc_allow_group` 时读取状态。
@@ -344,24 +346,151 @@ rules: # 自上而下,**首个命中生
344
346
 
345
347
  ## 编程接口
346
348
 
349
+ `proxycore.api` 是与 CLI 共用同一套配置、规则引擎和会话表的 SDK。
350
+ 完整教程见 [`docs/15-编程接口与SDK.md`](docs/15-编程接口与SDK.md)。
351
+
352
+ ### 30 秒:零配置启动
353
+
354
+ ```python
355
+ from proxycore.api import start_proxy
356
+
357
+ with start_proxy() as ppx: # 不需要任何配置文件
358
+ print(ppx.config_source) # "packaged"
359
+ print(ppx.config_path) # .../proxycore/data/default.yaml
360
+ print(ppx.status()) # 后端 / 规则 / 会话 / UDP / 配置来源
361
+ ```
362
+
363
+ 不传配置时的来源优先级:
364
+
365
+ | 优先级 | 来源 | 说明 |
366
+ |---|---|---|
367
+ | 1 | `start_proxy(path)` | 显式路径(也接受内存中的 `Config` 对象) |
368
+ | 2 | `PROCPROXY_CONFIG` | 指向不存在文件会直接报错,不回退 |
369
+ | 3 | `~/.procproxy/config.yaml` | `procproxy init` 生成的用户配置 |
370
+ | 4 | 包内 `data/default.yaml` | `backend=null`,不抓包、不改道 |
371
+
372
+ 内置配置**不会**监听 sink,也**不会**创建任何文件。它适合装配、规则决策、
373
+ 状态查询和联调;要真实抓包请用 `backend="windivert"`(需管理员)或
374
+ `procproxy init` 生成自己的配置。
375
+
376
+ ### 显式配置与测试转发
377
+
347
378
  ```python
348
- from proxycore import api
379
+ from proxycore.api import start_proxy
349
380
 
350
- # 上下文管理器:退出自动停
351
- with api.start_proxy("samples/profiles/dev.yaml") as handle:
352
- print(handle.status())
381
+ # 显式路径 + 真实抓包(需要管理员)
382
+ with start_proxy(r"D:\cfg\procproxy.yaml", backend="windivert") as ppx:
383
+ print(ppx.sink) # sink 监听地址
384
+ print(ppx.sessions(limit=20))
353
385
 
354
- # 测试模式:sink 把所有连接转发到固定目标(不做抓包)
355
- with api.start_proxy("samples/profiles/dev.yaml", backend="null",
356
- sink_test_target="127.0.0.1:8080") as handle:
386
+ # 联调模式:backend=null + 把 sink 收到的连接固定转发到目标
387
+ with start_proxy(backend="null", sink_test_target="127.0.0.1:8080",
388
+ upstream="local-mock") as ppx:
357
389
  ...
390
+ # curl -x http://127.0.0.1:19080 http://any/ → 127.0.0.1:8080
391
+ ```
392
+
393
+ ### 在代码里构建配置
394
+
395
+ 可以从字典构造 `Config`,也可以先落盘再按路径加载:
396
+
397
+ ```python
398
+ from proxycore.config import Config, atomic_write
399
+ from proxycore.api import start_proxy
400
+
401
+ doc = {
402
+ "version": 1,
403
+ "service": {"sink": {"host": "127.0.0.1", "port": 19080}, "backend": "null"},
404
+ "profile": {
405
+ "name": "myapp",
406
+ "default_action": "direct",
407
+ "dns_mode": "remote",
408
+ "rules": [
409
+ {"id": "r1", "action": "proxy", "upstream": "office",
410
+ "targets": "chrome.exe;msedge.exe", "dst_port": "443,80"},
411
+ ],
412
+ },
413
+ "upstreams": [
414
+ {"alias": "office", "kind": "socks5", "host": "127.0.0.1", "port": 1080},
415
+ ],
416
+ }
417
+
418
+ cfg = Config.from_dict(doc)
419
+ cfg.validate() # from_dict 只解析,不校验
420
+ atomic_write(cfg, "myconfig.yaml") # 临时文件 → 校验 → 原子替换
421
+
422
+ with start_proxy("myconfig.yaml") as ppx:
423
+ ...
424
+ ```
425
+
426
+ > `Config.from_dict()` 不会解析 `password_ref`;需要凭据时请用
427
+ > `Config.load()` / `start_proxy(path)`,或自己给 `upstream.password` 赋值。
358
428
 
359
- # 只要规则决策,不要监听
360
- runtime = api.make_runtime("samples/profiles/dev.yaml", backend="null")
429
+ ### 只做决策,不启动数据面
430
+
431
+ ```python
432
+ from proxycore.api import make_runtime
433
+
434
+ runtime = make_runtime() # 默认用内置配置,backend=null
361
435
  decision = runtime.decide(conn_request)
436
+ print(decision.action, decision.reason)
437
+ ```
438
+
439
+ ### 生命周期与句柄
440
+
441
+ ```python
442
+ ppx = start_proxy()
443
+ try:
444
+ print(ppx.status())
445
+ finally:
446
+ ppx.stop() # 幂等;with 退出时也会自动调用
447
+ ```
448
+
449
+ | 成员 | 说明 |
450
+ |---|---|
451
+ | `status()` | 后端、规则、会话、熔断、DNS、UDP、sink、`config_path`、`config_source` |
452
+ | `sessions(limit=50)` / `active_sessions()` | 会话明细 / 活跃数 |
453
+ | `reload(path="")` | 热重载;不传路径则重读当前文件。`sink.port`/`backend` 变更需重启 |
454
+ | `decide(conn)` | 只问规则引擎,不抓包、不改道 |
455
+ | `profile` | 当前生效的 `Profile` |
456
+ | `config_path` / `config_source` | 当前配置的绝对路径与来源 |
457
+ | `running` | 是否仍在运行 |
458
+
459
+ `start_proxy()` 会启动后台维护线程;`run_forever()` 会阻塞到 Ctrl+C。
460
+ 请在主线程使用,并在退出时 `stop()`(或使用 `with`)。
461
+
462
+ ### 错误处理
463
+
464
+ 配置 / 后端 / 启动失败统一抛 `ProxyStartError`,`code` 是稳定的机器可读标识:
465
+
466
+ ```python
467
+ from proxycore.api import ProxyStartError, start_proxy
468
+
469
+ try:
470
+ start_proxy("missing.yaml")
471
+ except ProxyStartError as exc:
472
+ print(exc.code) # config_not_found
362
473
  ```
363
474
 
364
- 跑 `python tests/tools/api_demo.py` 看三种用法的完整演示。
475
+ | code | 含义 |
476
+ |---|---|
477
+ | `config_not_found` | 配置文件不存在(显式路径 / 环境变量不会回退) |
478
+ | `config_unreadable` | 文件存在但读不了(权限、目录等) |
479
+ | `config_parse_invalid` | YAML 语法错误 / 根节点非映射 / 非 UTF-8 |
480
+ | `config_version_unsupported` | `version != 1` |
481
+ | `rule_upstream_missing` 等 | 规则或上游校验失败(完整列表见文档) |
482
+ | `E_BACKEND_UNAVAILABLE` | 后端不可用(未知后端、WinDivert 不可加载等) |
483
+
484
+ `api.load_config()` 同样抛 `ProxyStartError`,不是 `ConfigError`。
485
+
486
+ ### 环境变量
487
+
488
+ | 变量 | 作用 |
489
+ |---|---|
490
+ | `PROCPROXY_CONFIG` | 配置文件路径;显式路径之后、用户配置之前生效 |
491
+ | `PROCPROXY_RUNTIME_DIR` | 运行时状态/日志目录(CLI 与服务使用) |
492
+
493
+ 跑 `python tests/tools/api_demo.py` 看完整演示。
365
494
 
366
495
  ---
367
496
 
@@ -406,12 +535,13 @@ src/proxycore/ 核心包
406
535
  ├── tunnel/ sink + relay(本地落地与双向转发)
407
536
  ├── upstream/ SOCKS5 / SOCKS4 / HTTP CONNECT / 跳链
408
537
  ├── dns/ DNS 三模式与 UDP
538
+ ├── config_source.py 配置来源解析(显式 / env / 用户 / 样例 / 内置)
409
539
  ├── forward.py ← procproxy.py <进程名> 的实现
410
540
  ├── canproxy.py / checkroute.py / probe.py / pathsetup.py ← 其余子命令
411
541
  └── service/ ipc.py Windows 服务 + 命名管道
412
542
  samples/ mock 服务 + 示例配置
413
- docs/ 文档(00 架构与阅读路线 / 07 规则语义 / 11 排错 / 14 转发到抓包工具)
414
- tests/ 测试套件(555 passed)
543
+ docs/ 文档(00 架构 / 07 规则 / 11 排错 / 14 抓包 / 15 SDK)
544
+ tests/ 测试套件(619 passed / 4 skipped)
415
545
  └── tools/ 验证脚本(run_demo / run_e2e / bench / …)
416
546
  ```
417
547
 
@@ -442,7 +572,7 @@ tests/ 测试套件(555 passed)
442
572
 
443
573
  ```bash
444
574
  pip install -e ".[dev]"
445
- python -m pytest -q tests/ # 555 passed / 4 skipped(非管理员 Windows)
575
+ python -m pytest -q tests/ # 619 passed / 4 skipped(非管理员 Windows)
446
576
  python -m ruff check src tests
447
577
  python -m mypy src/proxycore
448
578
  ```
@@ -14,6 +14,7 @@ src/proxycore/canproxy.py
14
14
  src/proxycore/checkroute.py
15
15
  src/proxycore/cli.py
16
16
  src/proxycore/config.py
17
+ src/proxycore/config_source.py
17
18
  src/proxycore/forward.py
18
19
  src/proxycore/ipc.py
19
20
  src/proxycore/log.py
@@ -2,6 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.1.1"
5
+ __version__ = "0.1.2"
6
6
 
7
7
  __all__ = ["__version__"]