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.
- package/package.json +1 -1
- package/skills/sophnet-age-appearance/skill.json +9 -2
- package/skills/sophnet-age-appearance/src/scripts/age_appearance.py +8 -10
- package/skills/sophnet-id-photo/skill.json +9 -2
- package/skills/sophnet-id-photo/src/scripts/id_photo.py +5 -7
- package/skills/sophnet-image-edit/skill.json +9 -2
- package/skills/sophnet-image-edit/src/scripts/edit_image.py +5 -7
- package/skills/sophnet-image-generate/skill.json +9 -2
- package/skills/sophnet-image-generate/src/scripts/generate_image.py +51 -1
- package/skills/sophnet-infinite-talk/skill.json +9 -2
- package/skills/sophnet-infinite-talk/src/scripts/gen.py +39 -0
- package/skills/sophnet-invoice-assistant/skill.json +21 -0
- package/skills/sophnet-invoice-assistant/src/SKILL.md +186 -0
- package/skills/sophnet-invoice-assistant/src/pyproject.toml +8 -0
- package/skills/sophnet-invoice-assistant/src/references/api-contract.md +73 -0
- package/skills/sophnet-invoice-assistant/src/references/conversation-examples.md +16 -0
- package/skills/sophnet-invoice-assistant/src/references/error-cases.md +16 -0
- package/skills/sophnet-invoice-assistant/src/scripts/__init__.py +1 -0
- package/skills/sophnet-invoice-assistant/src/scripts/auth_context.py +74 -0
- package/skills/sophnet-invoice-assistant/src/scripts/cli.py +136 -0
- package/skills/sophnet-invoice-assistant/src/scripts/download_flow.py +89 -0
- package/skills/sophnet-invoice-assistant/src/scripts/dto.py +46 -0
- package/skills/sophnet-invoice-assistant/src/scripts/invoice_client.py +164 -0
- package/skills/sophnet-invoice-assistant/src/scripts/output.py +63 -0
- package/skills/sophnet-invoice-assistant/src/scripts/prepare_flow.py +58 -0
- package/skills/sophnet-invoice-assistant/src/scripts/risk_rules.py +95 -0
- package/skills/sophnet-invoice-assistant/src/scripts/status_flow.py +69 -0
- package/skills/sophnet-invoice-assistant/src/scripts/submit_flow.py +472 -0
- package/skills/sophnet-invoice-assistant/src/scripts/template_flow.py +196 -0
- package/skills/sophnet-invoice-assistant/src/scripts/update_flow.py +223 -0
- package/skills/sophnet-sticker-edit/skill.json +9 -2
- package/skills/sophnet-sticker-edit/src/scripts/edit_sticker_image.py +5 -7
- package/skills/sophnet-video-generate/skill.json +9 -2
- package/skills/sophnet-video-generate/src/SKILL.md +21 -12
- package/skills/sophnet-video-generate/src/scripts/gen_video.py +10 -15
- package/skills/store-marketing/skill.json +9 -2
- 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.
|
|
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-
|
|
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
|
|
285
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
69
|
-
5.
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
306
|
-
print(f"
|
|
307
|
-
|
|
308
|
-
|
|
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.
|
|
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-
|
|
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
|
-
|
|
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:
|