sophhub 0.4.45 → 0.4.47

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 (37) hide show
  1. package/package.json +1 -1
  2. package/skills/sophnet-age-appearance/skill.json +9 -2
  3. package/skills/sophnet-age-appearance/src/scripts/age_appearance.py +8 -10
  4. package/skills/sophnet-id-photo/skill.json +9 -2
  5. package/skills/sophnet-id-photo/src/scripts/id_photo.py +5 -7
  6. package/skills/sophnet-image-edit/skill.json +9 -2
  7. package/skills/sophnet-image-edit/src/scripts/edit_image.py +5 -7
  8. package/skills/sophnet-image-generate/skill.json +9 -2
  9. package/skills/sophnet-image-generate/src/scripts/generate_image.py +51 -1
  10. package/skills/sophnet-infinite-talk/skill.json +9 -2
  11. package/skills/sophnet-infinite-talk/src/scripts/gen.py +39 -0
  12. package/skills/sophnet-invoice-assistant/skill.json +21 -0
  13. package/skills/sophnet-invoice-assistant/src/SKILL.md +186 -0
  14. package/skills/sophnet-invoice-assistant/src/pyproject.toml +8 -0
  15. package/skills/sophnet-invoice-assistant/src/references/api-contract.md +73 -0
  16. package/skills/sophnet-invoice-assistant/src/references/conversation-examples.md +16 -0
  17. package/skills/sophnet-invoice-assistant/src/references/error-cases.md +16 -0
  18. package/skills/sophnet-invoice-assistant/src/scripts/__init__.py +1 -0
  19. package/skills/sophnet-invoice-assistant/src/scripts/auth_context.py +74 -0
  20. package/skills/sophnet-invoice-assistant/src/scripts/cli.py +136 -0
  21. package/skills/sophnet-invoice-assistant/src/scripts/download_flow.py +89 -0
  22. package/skills/sophnet-invoice-assistant/src/scripts/dto.py +46 -0
  23. package/skills/sophnet-invoice-assistant/src/scripts/invoice_client.py +164 -0
  24. package/skills/sophnet-invoice-assistant/src/scripts/output.py +63 -0
  25. package/skills/sophnet-invoice-assistant/src/scripts/prepare_flow.py +58 -0
  26. package/skills/sophnet-invoice-assistant/src/scripts/risk_rules.py +95 -0
  27. package/skills/sophnet-invoice-assistant/src/scripts/status_flow.py +69 -0
  28. package/skills/sophnet-invoice-assistant/src/scripts/submit_flow.py +472 -0
  29. package/skills/sophnet-invoice-assistant/src/scripts/template_flow.py +196 -0
  30. package/skills/sophnet-invoice-assistant/src/scripts/update_flow.py +223 -0
  31. package/skills/sophnet-sticker-edit/skill.json +9 -2
  32. package/skills/sophnet-sticker-edit/src/scripts/edit_sticker_image.py +5 -7
  33. package/skills/sophnet-video-generate/skill.json +9 -2
  34. package/skills/sophnet-video-generate/src/SKILL.md +21 -12
  35. package/skills/sophnet-video-generate/src/scripts/gen_video.py +10 -15
  36. package/skills/store-marketing/skill.json +9 -2
  37. package/skills/store-marketing/src/scripts/generate_poster.py +39 -1
@@ -0,0 +1,196 @@
1
+ """Invoice template management flows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoice_client import InvoiceClientError
6
+ from output import build_result, failure, need_confirmation
7
+ from submit_flow import (
8
+ _find_latest_template,
9
+ _find_template_by_name,
10
+ _normalize_tax_no,
11
+ _normalize_text,
12
+ _parse_buyer_type,
13
+ _parse_invoice_type,
14
+ _validate_fields,
15
+ )
16
+
17
+
18
+ def _resolve_template_payload(request, existing_template=None):
19
+ buyer_type = _parse_buyer_type(request)
20
+ invoice_type = _parse_invoice_type(request, buyer_type)
21
+ title = _normalize_text(request.title) or _normalize_text(existing_template.get("title") if existing_template else None)
22
+ tax_no = _normalize_tax_no(request.tax_no) or _normalize_tax_no(existing_template.get("taxNo") if existing_template else None)
23
+ address_phone = _normalize_text(request.address_phone) or _normalize_text(existing_template.get("addressPhone") if existing_template else None)
24
+ bank_account = _normalize_text(request.bank_account) or _normalize_text(existing_template.get("bankAccount") if existing_template else None)
25
+ template_name = (
26
+ _normalize_text(request.template_name)
27
+ or _normalize_text(existing_template.get("templateName") if existing_template else None)
28
+ or title
29
+ )
30
+
31
+ missing_fields = _validate_fields(
32
+ buyer_type,
33
+ invoice_type,
34
+ title,
35
+ tax_no,
36
+ address_phone,
37
+ bank_account,
38
+ )
39
+ if not template_name:
40
+ missing_fields.append("templateName")
41
+ if missing_fields:
42
+ return None, missing_fields
43
+
44
+ payload = {
45
+ "templateName": template_name,
46
+ "buyerType": buyer_type,
47
+ "title": title,
48
+ "invoiceType": invoice_type,
49
+ }
50
+ if tax_no:
51
+ payload["taxNo"] = tax_no
52
+ if address_phone:
53
+ payload["addressPhone"] = address_phone
54
+ if bank_account:
55
+ payload["bankAccount"] = bank_account
56
+ return payload, []
57
+
58
+
59
+ def run_template_list(request, auth_context, client):
60
+ if not auth_context.is_authenticated:
61
+ return failure(
62
+ action=request.action,
63
+ error_code="AUTH_CONTEXT_MISSING",
64
+ error_message="Current Sophnet auth context is missing. Please log in again.",
65
+ )
66
+
67
+ try:
68
+ org = client.get_current_personal_org(request.org_id)
69
+ templates = client.list_invoice_templates(org.get("id")) or []
70
+ except InvoiceClientError as exc:
71
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
72
+
73
+ matched = templates
74
+ if _normalize_text(request.template_name):
75
+ matched = [
76
+ item for item in templates
77
+ if _normalize_text(item.get("templateName")) == _normalize_text(request.template_name)
78
+ ]
79
+
80
+ latest_template = _find_latest_template(templates)
81
+ return build_result(
82
+ status="succeeded",
83
+ action=request.action,
84
+ message="Invoice templates were loaded successfully.",
85
+ extra={
86
+ "ORG_ID": str(org.get("id")),
87
+ "ORG_NAME": org.get("name") or "",
88
+ "TEMPLATE_COUNT": str(len(matched)),
89
+ "LATEST_TEMPLATE_NAME": _normalize_text(latest_template.get("templateName") if latest_template else None) or "",
90
+ "TEMPLATES": matched,
91
+ },
92
+ )
93
+
94
+
95
+ def run_template_save(request, auth_context, client):
96
+ if not auth_context.is_authenticated:
97
+ return failure(
98
+ action=request.action,
99
+ error_code="AUTH_CONTEXT_MISSING",
100
+ error_message="Current Sophnet auth context is missing. Please log in again.",
101
+ )
102
+
103
+ try:
104
+ org = client.get_current_personal_org(request.org_id)
105
+ templates = client.list_invoice_templates(org.get("id")) or []
106
+ existing_template = _find_template_by_name(templates, request.template_name)
107
+ payload, missing_fields = _resolve_template_payload(request, existing_template)
108
+ if payload is None:
109
+ return failure(
110
+ action=request.action,
111
+ error_code="MISSING_REQUIRED_FIELDS",
112
+ error_message="Missing required template fields: {0}".format(",".join(missing_fields)),
113
+ extra={"ORG_ID": str(org.get("id"))},
114
+ )
115
+ template = client.save_invoice_template(org.get("id"), payload)
116
+ except InvoiceClientError as exc:
117
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
118
+
119
+ return build_result(
120
+ status="succeeded",
121
+ action=request.action,
122
+ message="Invoice template was saved successfully.",
123
+ extra={
124
+ "ORG_ID": str(org.get("id")),
125
+ "ORG_NAME": org.get("name") or "",
126
+ "TEMPLATE_ID": str(template.get("id") or ""),
127
+ "TEMPLATE_NAME": template.get("templateName") or payload.get("templateName") or "",
128
+ "BUYER_TYPE": template.get("buyerType") or payload.get("buyerType") or "",
129
+ "INVOICE_TYPE": template.get("invoiceType") or payload.get("invoiceType") or "",
130
+ "TITLE": template.get("title") or payload.get("title") or "",
131
+ },
132
+ )
133
+
134
+
135
+ def run_template_delete(request, auth_context, client):
136
+ if not auth_context.is_authenticated:
137
+ return failure(
138
+ action=request.action,
139
+ error_code="AUTH_CONTEXT_MISSING",
140
+ error_message="Current Sophnet auth context is missing. Please log in again.",
141
+ )
142
+
143
+ try:
144
+ org = client.get_current_personal_org(request.org_id)
145
+ templates = client.list_invoice_templates(org.get("id")) or []
146
+ except InvoiceClientError as exc:
147
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
148
+
149
+ target = None
150
+ if _normalize_text(request.template_id):
151
+ for item in templates:
152
+ if str(item.get("id")) == str(request.template_id):
153
+ target = item
154
+ break
155
+ elif _normalize_text(request.template_name):
156
+ candidates = [
157
+ item for item in templates
158
+ if _normalize_text(item.get("templateName")) == _normalize_text(request.template_name)
159
+ ]
160
+ if len(candidates) > 1:
161
+ return need_confirmation(
162
+ action=request.action,
163
+ summary="Multiple templates matched the provided name.",
164
+ message="Please specify templateId before deleting the invoice template.",
165
+ extra={
166
+ "ORG_ID": str(org.get("id")),
167
+ "CANDIDATE_TEMPLATE_IDS": ",".join([str(item.get("id") or "") for item in candidates]),
168
+ },
169
+ )
170
+ if candidates:
171
+ target = candidates[0]
172
+
173
+ if target is None:
174
+ return failure(
175
+ action=request.action,
176
+ error_code="TEMPLATE_NOT_FOUND",
177
+ error_message="The target invoice template was not found.",
178
+ extra={"ORG_ID": str(org.get("id"))},
179
+ )
180
+
181
+ try:
182
+ client.delete_invoice_template(org.get("id"), target.get("id"))
183
+ except InvoiceClientError as exc:
184
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
185
+
186
+ return build_result(
187
+ status="succeeded",
188
+ action=request.action,
189
+ message="Invoice template was deleted successfully.",
190
+ extra={
191
+ "ORG_ID": str(org.get("id")),
192
+ "ORG_NAME": org.get("name") or "",
193
+ "TEMPLATE_ID": str(target.get("id") or ""),
194
+ "TEMPLATE_NAME": target.get("templateName") or "",
195
+ },
196
+ )
@@ -0,0 +1,223 @@
1
+ """Update invoice application flow."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from invoice_client import InvoiceClientError
6
+ from output import build_result, failure, need_confirmation
7
+ from submit_flow import (
8
+ INVOICE_ELIGIBLE_START_DATE,
9
+ VALID_ISSUING_MODES,
10
+ _filter_eligible_records,
11
+ _infer_recharge_records,
12
+ _normalize_tax_no,
13
+ _normalize_text,
14
+ _normalize_trans_nos,
15
+ _parse_buyer_type,
16
+ _parse_invoice_type,
17
+ _resolve_issuing_mode,
18
+ _select_record_map,
19
+ _sum_amount,
20
+ _validate_fields,
21
+ )
22
+
23
+
24
+ def _resolve_target_application(request, org_id, client):
25
+ if _normalize_text(request.application_no):
26
+ return client.get_invoice_application(org_id, request.application_no)
27
+
28
+ page = client.list_invoice_applications(org_id, params={"pageNum": 1, "pageSize": 20})
29
+ items = page.get("list") or [] if isinstance(page, dict) else []
30
+ if not items:
31
+ raise InvoiceClientError("APPLICATION_NOT_FOUND", "No invoice applications were found for the current account.")
32
+ if len(items) == 1:
33
+ return items[0]
34
+ raise InvoiceClientError("APPLICATION_CONFIRM_REQUIRED", "Please specify applicationNo before updating the invoice application.")
35
+
36
+
37
+ def _build_update_payload(request, application, records):
38
+ buyer_type = request.buyer_type or application.get("buyerType") or _parse_buyer_type(request)
39
+ invoice_type = request.invoice_type or application.get("invoiceType") or _parse_invoice_type(request, buyer_type)
40
+ title = _normalize_text(request.title) or _normalize_text(application.get("title"))
41
+ tax_no = _normalize_tax_no(request.tax_no) or _normalize_tax_no(application.get("taxNo"))
42
+ address_phone = _normalize_text(request.address_phone) or _normalize_text(application.get("addressPhone"))
43
+ bank_account = _normalize_text(request.bank_account) or _normalize_text(application.get("bankAccount"))
44
+
45
+ explicit_trans_nos = _normalize_trans_nos(request.recharge_trans_nos)
46
+ if explicit_trans_nos:
47
+ trans_nos = explicit_trans_nos
48
+ trans_no_error = None
49
+ else:
50
+ trans_nos = _normalize_trans_nos(application.get("rechargeTransNos"))
51
+ trans_no_error = None
52
+ if not trans_nos:
53
+ trans_nos, trans_no_error = _infer_recharge_records(request, records)
54
+
55
+ if trans_no_error:
56
+ return None, [trans_no_error], []
57
+
58
+ selected_records = _select_record_map(records, trans_nos)
59
+ if len(selected_records) != len(trans_nos):
60
+ return None, [
61
+ "One or more rechargeTransNos are not invoiceable. Recharge records before 2026-06-24 are not supported."
62
+ ], trans_nos
63
+
64
+ issuing_mode, issuing_mode_error = _resolve_issuing_mode(
65
+ request,
66
+ trans_nos,
67
+ application.get("issuingMode"),
68
+ )
69
+ if issuing_mode_error:
70
+ return None, [issuing_mode_error], trans_nos
71
+
72
+ missing_fields = _validate_fields(
73
+ buyer_type,
74
+ invoice_type,
75
+ title,
76
+ tax_no,
77
+ address_phone,
78
+ bank_account,
79
+ )
80
+ if not trans_nos:
81
+ missing_fields.append("rechargeTransNos")
82
+ if missing_fields:
83
+ return None, missing_fields, trans_nos
84
+
85
+ payload = {
86
+ "buyerType": buyer_type,
87
+ "title": title,
88
+ "invoiceType": invoice_type,
89
+ "rechargeTransNos": trans_nos,
90
+ }
91
+ if tax_no:
92
+ payload["taxNo"] = tax_no
93
+ if address_phone:
94
+ payload["addressPhone"] = address_phone
95
+ if bank_account:
96
+ payload["bankAccount"] = bank_account
97
+ if issuing_mode:
98
+ payload["issuingMode"] = issuing_mode
99
+ return payload, [], trans_nos
100
+
101
+
102
+ def run_update(request, auth_context, client):
103
+ if not auth_context.is_authenticated:
104
+ return failure(
105
+ action=request.action,
106
+ error_code="AUTH_CONTEXT_MISSING",
107
+ error_message="Current Sophnet auth context is missing. Please log in again.",
108
+ )
109
+
110
+ try:
111
+ org = client.get_current_personal_org(request.org_id)
112
+ application = _resolve_target_application(request, org.get("id"), client)
113
+ records_page = client.list_invoiceable_recharge_records(
114
+ org.get("id"),
115
+ params={"pageNum": 1, "pageSize": 100, "invoiceableOnly": "true"},
116
+ )
117
+ records = records_page.get("list") or [] if isinstance(records_page, dict) else []
118
+ eligible_records, ineligible_records = _filter_eligible_records(records)
119
+ payload, missing_fields, trans_nos = _build_update_payload(request, application, eligible_records)
120
+ except InvoiceClientError as exc:
121
+ if exc.code == "APPLICATION_CONFIRM_REQUIRED":
122
+ try:
123
+ page = client.list_invoice_applications(org.get("id"), params={"pageNum": 1, "pageSize": 10})
124
+ items = page.get("list") or [] if isinstance(page, dict) else []
125
+ except Exception:
126
+ items = []
127
+ return need_confirmation(
128
+ action=request.action,
129
+ summary="Multiple invoice applications are available for update.",
130
+ message="Please specify applicationNo before updating the invoice application.",
131
+ extra={
132
+ "ORG_ID": str(org.get("id")) if "org" in locals() and org else "",
133
+ "CANDIDATE_APPLICATION_NOS": ",".join([str(item.get("applicationNo") or "") for item in items[:10]]),
134
+ },
135
+ )
136
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
137
+
138
+ if payload is None:
139
+ if len(missing_fields) == 1 and "Multiple invoiceable recharge records" in missing_fields[0]:
140
+ available_records = [item for item in eligible_records if item.get("invoiceStatus") == "AVAILABLE"]
141
+ return need_confirmation(
142
+ action=request.action,
143
+ summary="Selectable recharge records: {0}".format(
144
+ ", ".join([str(item.get("transNo") or "") for item in available_records[:10]])
145
+ ),
146
+ message="Please specify rechargeTransNos before updating the invoice application.",
147
+ extra={
148
+ "ORG_ID": str(org.get("id")),
149
+ "APPLICATION_NO": application.get("applicationNo") or "",
150
+ "CANDIDATE_RECHARGE_TRANS_NOS": ",".join([str(item.get("transNo") or "") for item in available_records[:10]]),
151
+ },
152
+ )
153
+ if "issuingMode" in missing_fields:
154
+ available_records = [item for item in eligible_records if item.get("invoiceStatus") == "AVAILABLE"]
155
+ return need_confirmation(
156
+ action=request.action,
157
+ summary="Selected multiple recharge records. Please choose issuingMode=AGGREGATED or SEPARATE.",
158
+ message="When one invoice application contains multiple recharge records, you must choose aggregated or separate invoicing.",
159
+ extra={
160
+ "ORG_ID": str(org.get("id")),
161
+ "APPLICATION_NO": application.get("applicationNo") or "",
162
+ "CANDIDATE_RECHARGE_TRANS_NOS": ",".join(
163
+ [str(item.get("transNo") or "") for item in available_records[:10]]
164
+ ),
165
+ "AVAILABLE_ISSUING_MODES": ",".join(VALID_ISSUING_MODES),
166
+ },
167
+ )
168
+ return failure(
169
+ action=request.action,
170
+ error_code="MISSING_REQUIRED_FIELDS",
171
+ error_message="Missing required update fields: {0}".format(",".join(missing_fields)),
172
+ extra={
173
+ "ORG_ID": str(org.get("id")),
174
+ "APPLICATION_NO": application.get("applicationNo") or "",
175
+ "INELIGIBLE_RECHARGE_RECORD_COUNT": str(len(ineligible_records)),
176
+ },
177
+ )
178
+
179
+ selected_records = _select_record_map(eligible_records, trans_nos)
180
+ if not request.confirm:
181
+ return need_confirmation(
182
+ action=request.action,
183
+ summary="Update invoice application {0} with title={1}, invoiceType={2}, rechargeTransNos={3}, issuingMode={4}, amount={5:.2f}".format(
184
+ application.get("applicationNo") or "",
185
+ payload.get("title") or "",
186
+ payload.get("invoiceType") or "",
187
+ ",".join(payload.get("rechargeTransNos") or []),
188
+ payload.get("issuingMode") or "",
189
+ _sum_amount(selected_records),
190
+ ),
191
+ message="Please confirm the invoice application changes before updating.",
192
+ extra={
193
+ "ORG_ID": str(org.get("id")),
194
+ "APPLICATION_NO": application.get("applicationNo") or "",
195
+ "BUYER_TYPE": payload.get("buyerType") or "",
196
+ "TITLE": payload.get("title") or "",
197
+ "INVOICE_TYPE": payload.get("invoiceType") or "",
198
+ "RECHARGE_TRANS_NOS": ",".join(payload.get("rechargeTransNos") or []),
199
+ "ISSUING_MODE": payload.get("issuingMode") or "",
200
+ },
201
+ )
202
+
203
+ try:
204
+ updated = client.update_invoice_application(org.get("id"), application.get("applicationNo"), payload)
205
+ except InvoiceClientError as exc:
206
+ return failure(action=request.action, error_code=exc.code, error_message=exc.message)
207
+
208
+ return build_result(
209
+ status="succeeded",
210
+ action=request.action,
211
+ message="Invoice application was updated successfully.",
212
+ extra={
213
+ "ORG_ID": str(org.get("id")),
214
+ "ORG_NAME": org.get("name") or "",
215
+ "APPLICATION_NO": updated.get("applicationNo") or application.get("applicationNo") or "",
216
+ "APPLICATION_STATUS": updated.get("status") or application.get("status") or "",
217
+ "TITLE": updated.get("title") or payload.get("title") or "",
218
+ "AMOUNT": str(updated.get("amount") or application.get("amount") or ""),
219
+ "ISSUING_MODE": updated.get("issuingMode") or payload.get("issuingMode") or "",
220
+ "INELIGIBLE_RECHARGE_RECORD_COUNT": str(len(ineligible_records)),
221
+ "INVOICE_ELIGIBLE_START_DATE": INVOICE_ELIGIBLE_START_DATE,
222
+ },
223
+ )
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "sophnet-sticker-edit",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "types": [
5
5
  "store"
6
6
  ],
7
7
  "displayName": "表情包生成",
8
8
  "description": "根据输入的图片生成表情包",
9
9
  "changelog": [
10
+ {
11
+ "version": "1.0.3",
12
+ "date": "2026-06-24",
13
+ "changes": [
14
+ "修复 upload_oss 失败时临时文件泄漏;移除 PREVIEW_PATH 回退"
15
+ ]
16
+ },
10
17
  {
11
18
  "version": "1.0.2",
12
19
  "date": "2026-06-17",
@@ -30,5 +37,5 @@
30
37
  }
31
38
  ],
32
39
  "createdAt": "2026-05-13",
33
- "updatedAt": "2026-06-17"
40
+ "updatedAt": "2026-06-24"
34
41
  }
@@ -281,8 +281,9 @@ def reupload_for_signed_url(api_key, raw_url):
281
281
 
282
282
  signed_url = sophnet_tools.upload_oss(tmp_path)
283
283
  if not signed_url:
284
- print("Warning: upload_oss returned no signed URL, keeping local file", file=sys.stderr)
285
- return None, tmp_path
284
+ print("Warning: upload_oss returned no signed URL", file=sys.stderr)
285
+ os.unlink(tmp_path)
286
+ return None, None
286
287
 
287
288
  return signed_url, tmp_path
288
289
 
@@ -392,11 +393,8 @@ def main():
392
393
  for i, raw_url in enumerate(raw_urls, 1):
393
394
  signed_url, local_path = reupload_for_signed_url(api_key, raw_url)
394
395
  print(f"IMAGE_URL={signed_url or raw_url}")
395
- if local_path:
396
- if signed_url:
397
- os.unlink(local_path)
398
- else:
399
- print(f"PREVIEW_PATH={local_path}")
396
+ if signed_url and local_path:
397
+ os.unlink(local_path)
400
398
 
401
399
 
402
400
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sophnet-video-generate",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "types": [
5
5
  "builtin",
6
6
  "store"
@@ -8,6 +8,13 @@
8
8
  "displayName": "视频生成",
9
9
  "description": "通过 Sophnet API 生成视频,支持文生视频和图生视频",
10
10
  "changelog": [
11
+ {
12
+ "version": "1.0.5",
13
+ "date": "2026-06-24",
14
+ "changes": [
15
+ "输出格式改为结构化 key=value(STATUS/VIDEO_URL/VIDEO_LOCAL_PATH);上传成功后清理本地视频文件"
16
+ ]
17
+ },
11
18
  {
12
19
  "version": "1.0.4",
13
20
  "date": "2026-06-17",
@@ -48,5 +55,5 @@
48
55
  }
49
56
  ],
50
57
  "createdAt": "2026-04-09",
51
- "updatedAt": "2026-06-17"
58
+ "updatedAt": "2026-06-24"
52
59
  }
@@ -65,23 +65,32 @@ The script will:
65
65
  1. Upload local images to OSS if needed
66
66
  2. Submit the video generation request
67
67
  3. Poll the API until completion
68
- 4. Display the final video URL and local save path
69
- 5. Prompt the user to download the video
70
- 6. Lists other common models in console output; any documented Sophnet video model name can be passed via `--model`.
68
+ 4. Re-upload the video to OSS for a short signed URL, then clean up the local file
69
+ 5. Output structured key-value lines for machine parsing
71
70
 
72
- Example output:
71
+ Example output (success):
73
72
 
74
73
  ```
75
- 📹 **您的视频提示词为**: [prompt]
76
- ✅ **视频生成成功**: <video controls width="100%" src="https://www.sophnet.com/api/open-apis/projects/download/xyz789"></video>
77
- 📥 **视频本地保存路径**: /path/to/xyz789.mp4
78
- 💬 **提示**:
79
- ⚠️ 本内容有AI生成,视频保存24小时,请及时下载保存!
80
- 🤖 当前使用的模型为 [model],还支持 [other models]
81
- 🧠 如果想要指定模型,可以直接对我说:使用 [模型名称] 模型,生成 [视频描述]
74
+ STATUS=succeeded
75
+ VIDEO_URL=https://www.sophnet.com/api/open-apis/files/abc123
82
76
  ```
83
77
 
84
- Present this information to the user in a clear format.
78
+ If re-upload fails, an additional local path is provided:
79
+
80
+ ```
81
+ STATUS=succeeded
82
+ VIDEO_URL=https://raw-api.example.com/video/xyz789
83
+ VIDEO_LOCAL_PATH=/home/node/.openclaw/workspace/media/inbound/videos/task_id.mp4
84
+ ```
85
+
86
+ On failure:
87
+
88
+ ```
89
+ STATUS=failed
90
+ 视频生成失败
91
+ ```
92
+
93
+ Present the video to the user using the `VIDEO_URL` value. If `VIDEO_LOCAL_PATH` is present, mention the local backup location.
85
94
 
86
95
  ## Examples
87
96
 
@@ -236,7 +236,11 @@ def gen_video(
236
236
  # Re-upload to get a persistent URL
237
237
  result_video_url = sophnet_tools.upload_oss(local_video_url)
238
238
  if result_video_url:
239
- return result_video_url, local_video_url
239
+ try:
240
+ os.unlink(local_video_url)
241
+ except OSError:
242
+ pass
243
+ return result_video_url, None
240
244
  else:
241
245
  return video_url, local_video_url
242
246
  else:
@@ -302,20 +306,11 @@ if __name__ == "__main__":
302
306
  )
303
307
 
304
308
  if result_url:
305
- print(f"\n您的视频提示词为: {args.prompt}")
306
- print(f"视频生成成功: {result_url}")
307
- print(f"视频本地保存路径: {local_video_url}")
308
- print(f"提示: 视频保存24小时,请及时下载")
309
-
310
- # 生成支持的模型列表(排除当前使用的模型)
311
- other_models = [m for m in supported_models if m != model]
312
- other_models_str = "、".join(other_models)
313
- print(f"当前使用的模型为 {model},还支持 {other_models_str}")
314
- print(f"如果想要指定模型,可以直接对我说:使用 [模型名称] 模型,生成 [视频描述]")
315
- print(
316
- "提示: 常用模型示例: happyhorse-1.0-t2v、happyhorse-1.0-i2v、"
317
- "Wan2.6-T2V、Wan2.6-I2V、ViduQ2-pro、Seedance-2.0、Seedance-2.0-Fast、Seedance-1.5-Pro(也可传入文档中的其他模型名)"
318
- )
309
+ print(f"STATUS=succeeded")
310
+ print(f"VIDEO_URL={result_url}")
311
+ if local_video_url:
312
+ print(f"VIDEO_LOCAL_PATH={local_video_url}")
319
313
  else:
314
+ print(f"STATUS=failed")
320
315
  print("\n视频生成失败")
321
316
  sys.exit(1)
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "store-marketing",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "types": [
5
5
  "store"
6
6
  ],
7
7
  "displayName": "门店营销辅助",
8
8
  "description": "门店营销助手:为通用门店策划活动、分群触达、生成多渠道文案与海报。结合会员分群、目录价格、节日热点与老板风格。触发:营销/推广/促销/文案/海报/沉睡客户/活动方案/朋友圈/小红书/营销日历。",
9
9
  "changelog": [
10
+ {
11
+ "version": "1.1.5",
12
+ "date": "2026-06-24",
13
+ "changes": [
14
+ "海报生成后通过 sophnet_tools.upload_oss 重新上传,输出短链接避免被截断"
15
+ ]
16
+ },
10
17
  {
11
18
  "version": "1.1.4",
12
19
  "date": "2026-06-17",
@@ -70,7 +77,7 @@
70
77
  }
71
78
  ],
72
79
  "createdAt": "2026-05-29",
73
- "updatedAt": "2026-06-17",
80
+ "updatedAt": "2026-06-24",
74
81
  "resources": [
75
82
  "references",
76
83
  "playbooks"
@@ -19,7 +19,9 @@ from __future__ import annotations
19
19
  import argparse
20
20
  import json
21
21
  import math
22
+ import os
22
23
  import sys
24
+ import tempfile
23
25
  import time
24
26
  from typing import Any, Dict, List, Optional, Tuple
25
27
 
@@ -375,6 +377,41 @@ def extract_image_urls(data: Dict[str, Any]) -> List[str]:
375
377
  return urls
376
378
 
377
379
 
380
+ def reupload_image(api_key: str, raw_url: str) -> str:
381
+ """Download image from API URL and re-upload to OSS for short signed URL."""
382
+ headers = {"Authorization": f"Bearer {api_key}"}
383
+ try:
384
+ resp = requests.get(raw_url, headers=headers, timeout=120, stream=True)
385
+ resp.raise_for_status()
386
+ except requests.RequestException as e:
387
+ print(f"Warning: failed to download image: {e}", file=sys.stderr)
388
+ return raw_url
389
+
390
+ ext = os.path.splitext(raw_url.split("?")[0])[-1] or ".png"
391
+ fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="poster_gen_")
392
+ try:
393
+ with os.fdopen(fd, "wb") as f:
394
+ for chunk in resp.iter_content(8192):
395
+ f.write(chunk)
396
+ except IOError as e:
397
+ print(f"Warning: failed to write temp file: {e}", file=sys.stderr)
398
+ os.unlink(tmp_path)
399
+ return raw_url
400
+
401
+ import sophnet_tools as _st
402
+ signed_url = _st.upload_oss(tmp_path)
403
+ try:
404
+ os.unlink(tmp_path)
405
+ except OSError:
406
+ pass
407
+
408
+ if not signed_url:
409
+ print("Warning: upload_oss failed, using original URL", file=sys.stderr)
410
+ return raw_url
411
+
412
+ return signed_url
413
+
414
+
378
415
  def generate_image(
379
416
  api_key: str,
380
417
  prompt: str,
@@ -390,7 +427,8 @@ def generate_image(
390
427
  urls = extract_image_urls(result)
391
428
  if not urls:
392
429
  raise ValueError(f"url not found in response: {json.dumps(result, ensure_ascii=False)}")
393
- return task_id, urls[0]
430
+ image_url = reupload_image(api_key, urls[0])
431
+ return task_id, image_url
394
432
 
395
433
 
396
434
  def handle_http_error(e: requests.HTTPError) -> None: