sino-bc-cli 1.0.0-beta.0

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 (123) hide show
  1. package/README.md +256 -0
  2. package/dist/affordance/auth.md +31 -0
  3. package/dist/affordance/bill.md +48 -0
  4. package/dist/affordance/biz-line-ai-strategy.md +35 -0
  5. package/dist/affordance/biz-line-metrics.md +47 -0
  6. package/dist/affordance/biz-line.md +84 -0
  7. package/dist/affordance/common-file.md +25 -0
  8. package/dist/affordance/dashboard.md +75 -0
  9. package/dist/affordance/dict.md +28 -0
  10. package/dist/affordance/document.md +67 -0
  11. package/dist/affordance/im.md +91 -0
  12. package/dist/affordance/invoice.md +49 -0
  13. package/dist/affordance/media.md +17 -0
  14. package/dist/affordance/organization.md +47 -0
  15. package/dist/affordance/project-analysis.md +95 -0
  16. package/dist/affordance/project.md +321 -0
  17. package/dist/affordance/quota.md +19 -0
  18. package/dist/affordance/reconciliation.md +41 -0
  19. package/dist/affordance/report.md +75 -0
  20. package/dist/affordance/sso-team-group.md +30 -0
  21. package/dist/affordance/task.md +148 -0
  22. package/dist/affordance/tenant.md +17 -0
  23. package/dist/affordance/user.md +41 -0
  24. package/dist/affordance/wallet.md +100 -0
  25. package/dist/api-doc/README.md +67 -0
  26. package/dist/api-doc/custom/template.md +0 -0
  27. package/dist/api-doc/oapi/auth.md +61 -0
  28. package/dist/api-doc/oapi/bill.md +130 -0
  29. package/dist/api-doc/oapi/biz-line-ai-strategy.md +119 -0
  30. package/dist/api-doc/oapi/biz-line-metrics.md +419 -0
  31. package/dist/api-doc/oapi/biz-line.md +382 -0
  32. package/dist/api-doc/oapi/common-file.md +50 -0
  33. package/dist/api-doc/oapi/dashboard.md +218 -0
  34. package/dist/api-doc/oapi/dict.md +49 -0
  35. package/dist/api-doc/oapi/document.md +182 -0
  36. package/dist/api-doc/oapi/im.md +278 -0
  37. package/dist/api-doc/oapi/invoice.md +129 -0
  38. package/dist/api-doc/oapi/media.md +17 -0
  39. package/dist/api-doc/oapi/organization.md +94 -0
  40. package/dist/api-doc/oapi/project-analysis.md +364 -0
  41. package/dist/api-doc/oapi/project.md +1114 -0
  42. package/dist/api-doc/oapi/quota.md +21 -0
  43. package/dist/api-doc/oapi/reconciliation.md +98 -0
  44. package/dist/api-doc/oapi/report.md +254 -0
  45. package/dist/api-doc/oapi/sso-team-group.md +54 -0
  46. package/dist/api-doc/oapi/task.md +528 -0
  47. package/dist/api-doc/oapi/tenant.md +17 -0
  48. package/dist/api-doc/oapi/user.md +98 -0
  49. package/dist/api-doc/oapi/wallet.md +275 -0
  50. package/dist/generated/api-catalog.json +39681 -0
  51. package/dist/generated/api-directory-index.json +1509 -0
  52. package/dist/index.js +2 -0
  53. package/dist/skill-manifest.json +336 -0
  54. package/dist/skills/biz-line-strategy-generate/SKILL.md +122 -0
  55. package/dist/skills/biz-line-strategy-generate/references/ai-strategy-prompt.md +50 -0
  56. package/dist/skills/biz-line-strategy-generate/references/api-flow.md +155 -0
  57. package/dist/skills/biz-line-strategy-generate/references/four-strategies.md +58 -0
  58. package/dist/skills/biz-line-strategy-generate/references/metrics.md +119 -0
  59. package/dist/skills/biz-line-strategy-generate/references/output-excel.md +63 -0
  60. package/dist/skills/biz-line-strategy-generate/references/runtime.md +127 -0
  61. package/dist/skills/biz-line-strategy-generate/references/spec.md +500 -0
  62. package/dist/skills/biz-line-strategy-generate/scripts/ai_strategy.py +89 -0
  63. package/dist/skills/biz-line-strategy-generate/scripts/build_report.py +629 -0
  64. package/dist/skills/biz-line-strategy-generate/scripts/cleanup.py +87 -0
  65. package/dist/skills/biz-line-strategy-generate/scripts/cli_json.py +705 -0
  66. package/dist/skills/biz-line-strategy-generate/scripts/fetch_data.py +449 -0
  67. package/dist/skills/biz-line-strategy-generate/scripts/run_workflow.py +402 -0
  68. package/dist/skills/biz-line-strategy-generate/scripts/strategy_output.py +556 -0
  69. package/dist/skills/biz-line-strategy-generate/scripts/submit_tags.py +32 -0
  70. package/dist/skills/biz-line-strategy-generate/scripts/summarize.py +932 -0
  71. package/dist/skills/biz-line-strategy-generate/scripts/test_ai_strategy.py +235 -0
  72. package/dist/skills/biz-line-strategy-generate/scripts/test_cli_json.py +97 -0
  73. package/dist/skills/biz-line-strategy-generate/scripts/test_database_output.py +904 -0
  74. package/dist/skills/biz-line-strategy-generate/scripts/test_pipeline.py +253 -0
  75. package/dist/skills/biz-line-strategy-generate/scripts/test_strategy_rules.py +224 -0
  76. package/dist/skills/biz-line-strategy-generate/scripts/test_tag_submission.py +238 -0
  77. package/dist/skills/biz-line-strategy-generate/scripts/test_workflow.py +1233 -0
  78. package/dist/skills/sino-bc-cli/SKILL.md +62 -0
  79. package/dist/skills/sino-bc-cli/domains/sino-bc-auth/SKILL.md +36 -0
  80. package/dist/skills/sino-bc-cli/domains/sino-bc-bill/SKILL.md +48 -0
  81. package/dist/skills/sino-bc-cli/domains/sino-bc-biz-line/SKILL.md +58 -0
  82. package/dist/skills/sino-bc-cli/domains/sino-bc-biz-line/references/common-mistakes.md +7 -0
  83. package/dist/skills/sino-bc-cli/domains/sino-bc-biz-line/references/enums.md +13 -0
  84. package/dist/skills/sino-bc-cli/domains/sino-bc-biz-line-ai-strategy/SKILL.md +46 -0
  85. package/dist/skills/sino-bc-cli/domains/sino-bc-biz-line-metrics/SKILL.md +46 -0
  86. package/dist/skills/sino-bc-cli/domains/sino-bc-common-file/SKILL.md +44 -0
  87. package/dist/skills/sino-bc-cli/domains/sino-bc-dashboard/SKILL.md +50 -0
  88. package/dist/skills/sino-bc-cli/domains/sino-bc-dict/SKILL.md +48 -0
  89. package/dist/skills/sino-bc-cli/domains/sino-bc-document/SKILL.md +53 -0
  90. package/dist/skills/sino-bc-cli/domains/sino-bc-im/SKILL.md +62 -0
  91. package/dist/skills/sino-bc-cli/domains/sino-bc-invoice/SKILL.md +48 -0
  92. package/dist/skills/sino-bc-cli/domains/sino-bc-media/SKILL.md +40 -0
  93. package/dist/skills/sino-bc-cli/domains/sino-bc-organization/SKILL.md +46 -0
  94. package/dist/skills/sino-bc-cli/domains/sino-bc-project/SKILL.md +112 -0
  95. package/dist/skills/sino-bc-cli/domains/sino-bc-project-analysis/SKILL.md +61 -0
  96. package/dist/skills/sino-bc-cli/domains/sino-bc-project-analysis/references/analysis-workflow.md +21 -0
  97. package/dist/skills/sino-bc-cli/domains/sino-bc-project-analysis/references/filters-and-metrics.md +30 -0
  98. package/dist/skills/sino-bc-cli/domains/sino-bc-quota/SKILL.md +44 -0
  99. package/dist/skills/sino-bc-cli/domains/sino-bc-reconciliation/SKILL.md +47 -0
  100. package/dist/skills/sino-bc-cli/domains/sino-bc-report/SKILL.md +53 -0
  101. package/dist/skills/sino-bc-cli/domains/sino-bc-sso-team-group/SKILL.md +45 -0
  102. package/dist/skills/sino-bc-cli/domains/sino-bc-task/SKILL.md +73 -0
  103. package/dist/skills/sino-bc-cli/domains/sino-bc-task/references/state-machine.md +18 -0
  104. package/dist/skills/sino-bc-cli/domains/sino-bc-tenant/SKILL.md +34 -0
  105. package/dist/skills/sino-bc-cli/domains/sino-bc-user/SKILL.md +57 -0
  106. package/dist/skills/sino-bc-cli/domains/sino-bc-wallet/SKILL.md +55 -0
  107. package/dist/skills/sino-bc-cli/references/domain-index/module-8534789.md +53 -0
  108. package/dist/skills/sino-bc-cli/references/domain-index/module-8551546.md +70 -0
  109. package/dist/skills/sino-bc-cli/references/domain-index/module-8562638.md +13 -0
  110. package/dist/skills/sino-bc-cli/references/domain-index/module-8566367.md +13 -0
  111. package/dist/skills/sino-bc-cli/references/domain-index/module-8581524.md +17 -0
  112. package/dist/skills/sino-bc-cli/references/domain-index/module-8601540.md +13 -0
  113. package/dist/skills/sino-bc-cli/references/domain-index/module-8614891.md +33 -0
  114. package/dist/skills/sino-bc-cli/references/domain-index/module-8621792.md +17 -0
  115. package/dist/skills/sino-bc-shared/SKILL.md +31 -0
  116. package/dist/skills/sino-bc-shared/references/auth-recovery.md +36 -0
  117. package/dist/skills/sino-bc-shared/references/config-and-env.md +60 -0
  118. package/dist/skills/sino-bc-shared/references/output-contract.md +86 -0
  119. package/dist/skills/sino-bc-shared/references/skill-lifecycle.md +17 -0
  120. package/dist/skills/sino-bc-shared/references/tenant-context.md +21 -0
  121. package/dist/skills/sino-bc-shared/references/write-approval.md +16 -0
  122. package/package.json +70 -0
  123. package/scripts/postinstall.cjs +36 -0
@@ -0,0 +1,238 @@
1
+ """标签编号提交的真实文件、并发和命令行回归,不请求业务平台。"""
2
+ import json
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ import time
7
+ import unittest
8
+ from concurrent.futures import ThreadPoolExecutor
9
+ from pathlib import Path
10
+ from unittest.mock import patch
11
+
12
+ sys.dont_write_bytecode = True
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+
15
+ from strategy_output import choice_key, submit_tag_choices, close_tag_review, review_tags
16
+
17
+
18
+ class TagSubmission(unittest.TestCase):
19
+ """编号必须属于当前请求和策略类别,提交不可覆盖或复活目录。"""
20
+
21
+ def request(self, root, number=1, review='review-one'):
22
+ """三类候选故意共用真实ID,检验只核对ID无法发现的错投。"""
23
+ root.mkdir(exist_ok=True)
24
+ packet = {'bizLineId': 340, 'attempt': 1, 'reviewId': review,
25
+ 'expiresAt': time.time() + 420, 'submissionMode': 'choice-key-v1',
26
+ 'strategies': {key: {'strategy': '本条正文', 'labels': [
27
+ {'id': 1, 'name': '具体标签', 'choiceKey': choice_key(review, key, 1)},
28
+ {'id': 2, 'name': '其他', 'isOther': 1, 'choiceKey': choice_key(review, key, 2)}]}
29
+ for key in ('budget', 'product', 'audience')}}
30
+ path = root / f'tag-review.{number}.request.json'
31
+ path.write_text(json.dumps(packet, ensure_ascii=False), encoding='utf-8')
32
+ selection = {'selections': {key: [{'choiceKey': item['labels'][0]['choiceKey'],
33
+ 'reason': '本条策略对应具体动作'}]
34
+ for key, item in packet['strategies'].items()}}
35
+ return path, selection
36
+
37
+ def test_valid_maps_id_and_rejects_duplicate_publish(self):
38
+ with tempfile.TemporaryDirectory() as tmp:
39
+ path, choices = self.request(Path(tmp))
40
+ response = submit_tag_choices(path, 'review-one', choices)
41
+ value = json.loads(response.read_text(encoding='utf-8'))
42
+ self.assertEqual(value['bizLineId'], 340)
43
+ self.assertEqual(value['selections']['budget'][0]['id'], 1)
44
+ self.assertEqual(value['selections']['budget'][0]['choiceKey'],
45
+ choices['selections']['budget'][0]['choiceKey'])
46
+ before = response.read_bytes()
47
+ with self.assertRaises(ValueError):
48
+ submit_tag_choices(path, 'review-one', choices)
49
+ self.assertEqual(response.read_bytes(), before)
50
+
51
+ def test_foreign_request_scene_or_direct_id_cannot_publish(self):
52
+ for mutation in ('request', 'scene', 'id', 'reason', 'other'):
53
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as tmp:
54
+ path, choices = self.request(Path(tmp))
55
+ budget = choices['selections']['budget']
56
+ if mutation == 'request':
57
+ budget[0]['choiceKey'] = choice_key('another-review', 'budget', 1)
58
+ elif mutation == 'scene':
59
+ budget[0]['choiceKey'] = choices['selections']['product'][0]['choiceKey']
60
+ elif mutation == 'id':
61
+ budget[0] = {'id': 1, 'reason': '直接填ID'}
62
+ elif mutation == 'reason':
63
+ budget[0]['reason'] = ''
64
+ else:
65
+ budget.append({'choiceKey': choice_key('review-one', 'budget', 2), 'reason': '其他'})
66
+ with self.assertRaises(ValueError):
67
+ submit_tag_choices(path, 'review-one', choices)
68
+ self.assertFalse(path.with_name('tag-review.1.response.json').exists())
69
+
70
+ def test_helper_field_error_can_be_corrected_in_same_request(self):
71
+ """提示具体位置和字段;错误输入不落响应,修正后仍可提交同一请求。"""
72
+ for field, invalid in (('choiceKey', None), ('reason', None), ('choiceKey', 123), ('reason', '')):
73
+ with self.subTest(field=field, invalid=invalid), tempfile.TemporaryDirectory() as tmp:
74
+ path, choices = self.request(Path(tmp))
75
+ broken = json.loads(json.dumps(choices))
76
+ item = broken['selections']['budget'][0]
77
+ if invalid is None:
78
+ item.pop(field)
79
+ else:
80
+ item[field] = invalid
81
+ with self.assertRaises(ValueError) as caught:
82
+ submit_tag_choices(path, 'review-one', broken)
83
+ self.assertIn('budget[0]', str(caught.exception))
84
+ self.assertIn(field, str(caught.exception))
85
+ self.assertIn('submit_tags.py', str(caught.exception))
86
+ self.assertFalse(path.with_name('tag-review.1.response.json').exists())
87
+ response = submit_tag_choices(path, 'review-one', choices)
88
+ self.assertEqual(json.loads(response.read_text(encoding='utf-8'))['reviewId'], 'review-one')
89
+
90
+ def test_expired_replaced_closed_or_wrong_identity_cannot_publish(self):
91
+ for mutation in ('expired', 'replaced', 'closed', 'identity'):
92
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as tmp:
93
+ path, choices = self.request(Path(tmp))
94
+ expected = 'review-one'
95
+ if mutation == 'expired':
96
+ packet = json.loads(path.read_text(encoding='utf-8'))
97
+ packet['expiresAt'] = time.time() - 1
98
+ path.write_text(json.dumps(packet), encoding='utf-8')
99
+ elif mutation == 'replaced':
100
+ self.request(Path(tmp), 2, 'review-two')
101
+ elif mutation == 'closed':
102
+ close_tag_review(path)
103
+ else:
104
+ expected = 'another-review'
105
+ with self.assertRaises(ValueError):
106
+ submit_tag_choices(path, expected, choices)
107
+ self.assertFalse(path.with_name('tag-review.1.response.json').exists())
108
+
109
+ def test_submission_rereads_request_before_publish(self):
110
+ with tempfile.TemporaryDirectory() as tmp:
111
+ path, choices = self.request(Path(tmp))
112
+ from strategy_output import map_tag_choices
113
+ def changed(packet, selected):
114
+ mapped = map_tag_choices(packet, selected)
115
+ packet['strategies']['budget']['strategy'] = '请求在提交期间被改动'
116
+ path.write_text(json.dumps(packet), encoding='utf-8')
117
+ return mapped
118
+ with patch('strategy_output.map_tag_choices', side_effect=changed), self.assertRaises(ValueError):
119
+ submit_tag_choices(path, 'review-one', choices)
120
+ self.assertFalse(path.with_name('tag-review.1.response.json').exists())
121
+
122
+ def test_concurrent_publish_has_exactly_one_winner(self):
123
+ with tempfile.TemporaryDirectory() as tmp:
124
+ path, choices = self.request(Path(tmp))
125
+ def submit(_):
126
+ try:
127
+ submit_tag_choices(path, 'review-one', choices)
128
+ return True
129
+ except ValueError:
130
+ return False
131
+ with ThreadPoolExecutor(max_workers=3) as pool:
132
+ self.assertEqual(sum(pool.map(submit, range(3))), 1)
133
+ json.loads(path.with_name('tag-review.1.response.json').read_text(encoding='utf-8'))
134
+ self.assertFalse(list(Path(tmp).glob('*.tmp')))
135
+
136
+ def test_removed_root_is_not_recreated(self):
137
+ with tempfile.TemporaryDirectory() as tmp:
138
+ # 使用临时目录自身的生命周期模拟已结束业务线,不删用户目录。
139
+ from cleanup import OwnedDirectory
140
+ owned = OwnedDirectory.create(Path(tmp), 'owned-')
141
+ other_path, other_choices = self.request(owned.path)
142
+ self.assertTrue(owned.cleanup()['ok'])
143
+ with self.assertRaises((ValueError, OSError)):
144
+ submit_tag_choices(other_path, 'review-one', other_choices)
145
+ self.assertFalse(owned.path.exists())
146
+
147
+ def test_consumer_rejects_direct_ids_or_mismatched_mapping(self):
148
+ """即使绕过helper写文件,也不能仅凭合法ID通过实际审核消费。"""
149
+ import threading
150
+ for mutation in ('direct-id', 'foreign-code', 'wrong-id', 'boolean-id', 'missing-id', 'missing-reason'):
151
+ with self.subTest(mutation=mutation), tempfile.TemporaryDirectory() as tmp:
152
+ row = {key + 'Strategy': '本条完整正文' for key in ('budget', 'product', 'audience')}
153
+ dictionaries = {key: [{'id': 1, 'name': '具体'}, {'id': 2, 'name': '其他', 'isOther': 1}]
154
+ for key in ('budget', 'product', 'audience')}
155
+ events = []
156
+ def progress(event):
157
+ events.append(event)
158
+ if event.get('event') != 'tag_review_required':
159
+ return
160
+ packet = json.loads(Path(event['request_path']).read_text(encoding='utf-8'))
161
+ selection = {key: [{'id': 1, 'choiceKey': item['labels'][0]['choiceKey'], 'reason': '依据'}]
162
+ for key, item in packet['strategies'].items()}
163
+ item = selection['budget'][0]
164
+ if mutation == 'direct-id':
165
+ item.pop('choiceKey')
166
+ elif mutation == 'foreign-code':
167
+ item['choiceKey'] = choice_key('foreign', 'budget', 1)
168
+ elif mutation == 'missing-id':
169
+ item.pop('id')
170
+ elif mutation == 'missing-reason':
171
+ item.pop('reason')
172
+ else:
173
+ item['id'] = True if mutation == 'boolean-id' else 2
174
+ reply = {key: packet[key] for key in ('bizLineId', 'attempt', 'reviewId')}
175
+ Path(event['response_path']).write_text(json.dumps({**reply, 'selections': selection}), encoding='utf-8')
176
+ selected = review_tags(row, dictionaries, {}, Path(tmp), threading.Event(), progress,
177
+ biz_line_id=340, attempt=1)
178
+ rejected = [event for event in events if event['event'] == 'tag_review_rejected']
179
+ self.assertEqual(len(rejected), 2)
180
+ field = {'direct-id': 'choiceKey', 'boolean-id': 'id', 'missing-id': 'id', 'missing-reason': 'reason'}.get(mutation)
181
+ if field:
182
+ for event in rejected:
183
+ self.assertIn('budget[0]', event['reason'])
184
+ self.assertIn(field, event['reason'])
185
+ self.assertIn('submit_tags.py', event['reason'])
186
+ self.assertEqual(selected['budget'][0]['id'], 2)
187
+
188
+ def test_cancel_closes_request_and_rejects_late_submit(self):
189
+ """取消后即使原expiresAt尚未到期,也不能向旧请求补回包。"""
190
+ import threading
191
+ cancel = threading.Event()
192
+ seen = []
193
+ row = {key + 'Strategy': '正文' for key in ('budget', 'product', 'audience')}
194
+ dictionaries = {key: [{'id': 1, 'name': '其他'}] for key in ('budget', 'product', 'audience')}
195
+ with tempfile.TemporaryDirectory() as tmp:
196
+ def progress(event):
197
+ if event.get('event') == 'tag_review_required':
198
+ seen.append(event)
199
+ cancel.set()
200
+ with self.assertRaises(InterruptedError):
201
+ review_tags(row, dictionaries, {}, Path(tmp), cancel, progress, biz_line_id=340, attempt=1)
202
+ event = seen[0]
203
+ packet = json.loads(Path(event['request_path']).read_text(encoding='utf-8'))
204
+ choices = {'selections': {key: [{'choiceKey': item['labels'][0]['choiceKey'], 'reason': '迟到'}]
205
+ for key, item in packet['strategies'].items()}}
206
+ with self.assertRaises(ValueError):
207
+ submit_tag_choices(event['request_path'], event['reviewId'], choices)
208
+
209
+ def test_cli_real_utf8_arguments_and_stdin(self):
210
+ with tempfile.TemporaryDirectory() as tmp:
211
+ path, choices = self.request(Path(tmp))
212
+ result = subprocess.run([sys.executable, '-B', str(Path(__file__).with_name('submit_tags.py')),
213
+ '--request', str(path), '--review-id', 'review-one'],
214
+ input=json.dumps(choices, ensure_ascii=False), text=True,
215
+ encoding='utf-8', capture_output=True, timeout=10)
216
+ self.assertEqual(result.returncode, 0, result.stderr)
217
+ self.assertTrue(json.loads(result.stdout)['ok'])
218
+ self.assertIn('本条策略', path.with_name('tag-review.1.response.json').read_text(encoding='utf-8'))
219
+
220
+ def test_cli_reports_missing_field_in_stderr_without_publishing(self):
221
+ """实际CLI返回可修正的JSON错误,而非裸KeyError,也不提前生成响应。"""
222
+ with tempfile.TemporaryDirectory() as tmp:
223
+ path, choices = self.request(Path(tmp))
224
+ choices['selections']['budget'][0].pop('choiceKey')
225
+ result = subprocess.run([sys.executable, '-B', str(Path(__file__).with_name('submit_tags.py')),
226
+ '--request', str(path), '--review-id', 'review-one'],
227
+ input=json.dumps(choices, ensure_ascii=False), text=True,
228
+ encoding='utf-8', capture_output=True, timeout=10)
229
+ self.assertEqual(result.returncode, 1)
230
+ error = json.loads(result.stderr)
231
+ self.assertFalse(error['ok'])
232
+ for word in ('budget[0]', 'choiceKey', 'submit_tags.py'):
233
+ self.assertIn(word, error['error'])
234
+ self.assertFalse(path.with_name('tag-review.1.response.json').exists())
235
+
236
+
237
+ if __name__ == '__main__':
238
+ unittest.main()